Merge remote-tracking branch 'upstream/api-migration' into api-channel

This commit is contained in:
crobibero
2020-07-06 10:00:23 -06:00
82 changed files with 8256 additions and 5650 deletions

View File

@@ -35,17 +35,14 @@ namespace Jellyfin.Api.Controllers
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
/// <param name="hasUserId">Optional. Only returns activities that have a user associated.</param>
/// <response code="200">Activity log returned.</response>
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
[HttpGet("Entries")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "hasUserId", Justification = "Imported from ServiceStack")]
public ActionResult<QueryResult<ActivityLogEntry>> GetLogEntries(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] DateTime? minDate,
bool? hasUserId)
[FromQuery] DateTime? minDate)
{
var filterFunc = new Func<IQueryable<ActivityLog>, IQueryable<ActivityLog>>(
entries => entries.Where(entry => entry.DateCreated >= minDate));

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The albums controller.
/// </summary>
public class AlbumsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="AlbumsController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public AlbumsController(
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService)
{
_userManager = userManager;
_libraryManager = libraryManager;
_dtoService = dtoService;
}
/// <summary>
/// Finds albums similar to a given album.
/// </summary>
/// <param name="albumId">The album id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar albums returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar albums.</returns>
[HttpGet("/Albums/{albumId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarAlbums(
[FromRoute] string albumId,
[FromQuery] Guid userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
albumId,
excludeArtistIds,
limit,
new[] { typeof(MusicAlbum) },
GetAlbumSimilarityScore);
}
/// <summary>
/// Finds artists similar to a given artist.
/// </summary>
/// <param name="artistId">The artist id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="excludeArtistIds">Optional. Ids of artists to exclude.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <response code="200">Similar artists returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with similar artists.</returns>
[HttpGet("/Artists/{artistId}/Similar")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarArtists(
[FromRoute] string artistId,
[FromQuery] Guid userId,
[FromQuery] string? excludeArtistIds,
[FromQuery] int? limit)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
return SimilarItemsHelper.GetSimilarItemsResult(
dtoOptions,
_userManager,
_libraryManager,
_dtoService,
userId,
artistId,
excludeArtistIds,
limit,
new[] { typeof(MusicArtist) },
SimilarItemsHelper.GetSimiliarityScore);
}
/// <summary>
/// Gets a similairty score of two albums.
/// </summary>
/// <param name="item1">The first item.</param>
/// <param name="item1People">The item1 people.</param>
/// <param name="allPeople">All people.</param>
/// <param name="item2">The second item.</param>
/// <returns>System.Int32.</returns>
private int GetAlbumSimilarityScore(BaseItem item1, List<PersonInfo> item1People, List<PersonInfo> allPeople, BaseItem item2)
{
var points = SimilarItemsHelper.GetSimiliarityScore(item1, item1People, allPeople, item2);
var album1 = (MusicAlbum)item1;
var album2 = (MusicAlbum)item2;
var artists1 = album1
.GetAllArtists()
.DistinctNames()
.ToList();
var artists2 = new HashSet<string>(
album2.GetAllArtists().DistinctNames(),
StringComparer.OrdinalIgnoreCase);
return points + artists1.Where(artists2.Contains).Sum(i => 5);
}
}
}

View File

@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Keys")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateKey([FromQuery, Required] string app)
public ActionResult CreateKey([FromQuery, Required] string? app)
{
_authRepo.Create(new AuthenticationInfo
{
@@ -88,7 +88,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Keys/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RevokeKey([FromRoute] string key)
public ActionResult RevokeKey([FromRoute] string? key)
{
_sessionManager.RevokeToken(key);
return NoContent();

View File

@@ -0,0 +1,488 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The artists controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
[Route("/Artists")]
public class ArtistsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public ArtistsController(
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService)
{
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
}
/// <summary>
/// Gets all artists from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Artists returned.</response>
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string searchTerm,
[FromQuery] string parentId,
[FromQuery] string fields,
[FromQuery] string excludeItemTypes,
[FromQuery] string includeItemTypes,
[FromQuery] string filters,
[FromQuery] bool? isFavorite,
[FromQuery] string mediaTypes,
[FromQuery] string genres,
[FromQuery] string genreIds,
[FromQuery] string officialRatings,
[FromQuery] string tags,
[FromQuery] string years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string enableImageTypes,
[FromQuery] string person,
[FromQuery] string personIds,
[FromQuery] string personTypes,
[FromQuery] string studios,
[FromQuery] string studioIds,
[FromQuery] Guid userId,
[FromQuery] string nameStartsWithOrGreater,
[FromQuery] string nameStartsWith,
[FromQuery] string nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (!userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
Genres = RequestHelpers.Split(genres, ',', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
if (!string.IsNullOrWhiteSpace(parentId))
{
if (parentItem is Folder)
{
query.AncestorIds = new[] { new Guid(parentId) };
}
else
{
query.ItemIds = new[] { new Guid(parentId) };
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|').Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
foreach (var filter in RequestHelpers.GetFilters(filters))
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = _libraryManager.GetArtists(query);
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
}
/// <summary>
/// Gets all album artists from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Album artists returned.</response>
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
[HttpGet("AlbumArtists")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string searchTerm,
[FromQuery] string parentId,
[FromQuery] string fields,
[FromQuery] string excludeItemTypes,
[FromQuery] string includeItemTypes,
[FromQuery] string filters,
[FromQuery] bool? isFavorite,
[FromQuery] string mediaTypes,
[FromQuery] string genres,
[FromQuery] string genreIds,
[FromQuery] string officialRatings,
[FromQuery] string tags,
[FromQuery] string years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string enableImageTypes,
[FromQuery] string person,
[FromQuery] string personIds,
[FromQuery] string personTypes,
[FromQuery] string studios,
[FromQuery] string studioIds,
[FromQuery] Guid userId,
[FromQuery] string nameStartsWithOrGreater,
[FromQuery] string nameStartsWith,
[FromQuery] string nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (!userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
Genres = RequestHelpers.Split(genres, ',', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
if (!string.IsNullOrWhiteSpace(parentId))
{
if (parentItem is Folder)
{
query.AncestorIds = new[] { new Guid(parentId) };
}
else
{
query.ItemIds = new[] { new Guid(parentId) };
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|').Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null).Select(i => i!.Id).ToArray();
}
foreach (var filter in RequestHelpers.GetFilters(filters))
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = _libraryManager.GetAlbumArtists(query);
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
}
/// <summary>
/// Gets an artist by name.
/// </summary>
/// <param name="name">Studio name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <response code="200">Artist returned.</response>
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetArtistByName([FromRoute] string name, [FromQuery] Guid userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
var item = _libraryManager.GetArtist(name, dtoOptions);
if (!userId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(userId);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
}

View File

@@ -0,0 +1,110 @@
using System;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Collections;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The collection controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
[Route("/Collections")]
public class CollectionController : BaseJellyfinApiController
{
private readonly ICollectionManager _collectionManager;
private readonly IDtoService _dtoService;
private readonly IAuthorizationContext _authContext;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionController"/> class.
/// </summary>
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
public CollectionController(
ICollectionManager collectionManager,
IDtoService dtoService,
IAuthorizationContext authContext)
{
_collectionManager = collectionManager;
_dtoService = dtoService;
_authContext = authContext;
}
/// <summary>
/// Creates a new collection.
/// </summary>
/// <param name="name">The name of the collection.</param>
/// <param name="ids">Item Ids to add to the collection.</param>
/// <param name="isLocked">Whether or not to lock the new collection.</param>
/// <param name="parentId">Optional. Create the collection within a specific folder.</param>
/// <response code="200">Collection created.</response>
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<CollectionCreationResult> CreateCollection(
[FromQuery] string? name,
[FromQuery] string? ids,
[FromQuery] bool isLocked,
[FromQuery] Guid? parentId)
{
var userId = _authContext.GetAuthorizationInfo(Request).UserId;
var item = _collectionManager.CreateCollection(new CollectionCreationOptions
{
IsLocked = isLocked,
Name = name,
ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true),
UserIds = new[] { userId }
});
var dtoOptions = new DtoOptions().AddClientFields(Request);
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
return new CollectionCreationResult
{
Id = dto.Id
};
}
/// <summary>
/// Adds items to a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
/// <param name="itemIds">Item ids, comma delimited.</param>
/// <response code="204">Items added to collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddToCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
{
_collectionManager.AddToCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
return NoContent();
}
/// <summary>
/// Removes items from a collection.
/// </summary>
/// <param name="collectionId">The collection id.</param>
/// <param name="itemIds">Item ids, comma delimited.</param>
/// <response code="204">Items removed from collection.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveFromCollection([FromRoute] Guid collectionId, [FromQuery] string? itemIds)
{
_collectionManager.RemoveFromCollection(collectionId, RequestHelpers.Split(itemIds, ',', true));
return NoContent();
}
}
}

View File

@@ -70,7 +70,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>Configuration.</returns>
[HttpGet("Configuration/{key}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<object> GetNamedConfiguration([FromRoute] string key)
public ActionResult<object> GetNamedConfiguration([FromRoute] string? key)
{
return _configurationManager.GetConfiguration(key);
}
@@ -84,7 +84,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Configuration/{key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string? key)
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);

View File

@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("/web/ConfigurationPage")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult GetDashboardConfigurationPage([FromQuery] string name)
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
{
IPlugin? plugin = null;
Stream? stream = null;
@@ -178,14 +178,13 @@ namespace Jellyfin.Api.Controllers
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult GetRobotsTxt()
{
return GetWebClientResource("robots.txt", string.Empty);
return GetWebClientResource("robots.txt");
}
/// <summary>
/// Gets a resource from the web client.
/// </summary>
/// <param name="resourceName">The resource name.</param>
/// <param name="v">The v.</param>
/// <response code="200">Web client returned.</response>
/// <response code="404">Server does not host a web client.</response>
/// <returns>The resource.</returns>
@@ -193,10 +192,7 @@ namespace Jellyfin.Api.Controllers
[ApiExplorerSettings(IgnoreApi = true)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "v", Justification = "Imported from ServiceStack")]
public ActionResult GetWebClientResource(
[FromRoute] string resourceName,
[FromQuery] string? v)
public ActionResult GetWebClientResource([FromRoute] string resourceName)
{
if (!_appConfig.HostWebClient() || WebClientUiPath == null)
{
@@ -228,7 +224,7 @@ namespace Jellyfin.Api.Controllers
[ApiExplorerSettings(IgnoreApi = true)]
public ActionResult GetFavIcon()
{
return GetWebClientResource("favicon.ico", string.Empty);
return GetWebClientResource("favicon.ico");
}
/// <summary>

View File

@@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string id)
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, BindRequired] string? id)
{
var deviceInfo = _deviceManager.GetDevice(id);
if (deviceInfo == null)
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string id)
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, BindRequired] string? id)
{
var deviceInfo = _deviceManager.GetDeviceOptions(id);
if (deviceInfo == null)
@@ -111,7 +111,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
[FromQuery, BindRequired] string id,
[FromQuery, BindRequired] string? id,
[FromBody, BindRequired] DeviceOptions deviceOptions)
{
var existingDeviceOptions = _deviceManager.GetDeviceOptions(id);
@@ -134,7 +134,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
public ActionResult DeleteDevice([FromQuery, BindRequired] string? id)
{
var existingDevice = _deviceManager.GetDevice(id);
if (existingDevice == null)

View File

@@ -39,9 +39,9 @@ namespace Jellyfin.Api.Controllers
[HttpGet("{displayPreferencesId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<DisplayPreferences> GetDisplayPreferences(
[FromRoute] string displayPreferencesId,
[FromQuery] [Required] string userId,
[FromQuery] [Required] string client)
[FromRoute] string? displayPreferencesId,
[FromQuery] [Required] string? userId,
[FromQuery] [Required] string? client)
{
return _displayPreferencesRepository.GetDisplayPreferences(displayPreferencesId, userId, client);
}
@@ -59,9 +59,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
public ActionResult UpdateDisplayPreferences(
[FromRoute] string displayPreferencesId,
[FromQuery, BindRequired] string userId,
[FromQuery, BindRequired] string client,
[FromRoute] string? displayPreferencesId,
[FromQuery, BindRequired] string? userId,
[FromQuery, BindRequired] string? client,
[FromBody, BindRequired] DisplayPreferences displayPreferences)
{
_displayPreferencesRepository.SaveDisplayPreferences(

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.EnvironmentDtos;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Environment Controller.
/// </summary>
[Authorize(Policy = Policies.RequiresElevation)]
public class EnvironmentController : BaseJellyfinApiController
{
private const char UncSeparator = '\\';
private const string UncStartPrefix = @"\\";
private readonly IFileSystem _fileSystem;
private readonly ILogger<EnvironmentController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
/// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
{
_fileSystem = fileSystem;
_logger = logger;
}
/// <summary>
/// Gets the contents of a given directory in the file system.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
/// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
/// <response code="200">Directory contents returned.</response>
/// <returns>Directory contents.</returns>
[HttpGet("DirectoryContents")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
[FromQuery, BindRequired] string path,
[FromQuery] bool includeFiles = false,
[FromQuery] bool includeDirectories = false)
{
if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
&& path.LastIndexOf(UncSeparator) == 1)
{
return Array.Empty<FileSystemEntryInfo>();
}
var entries =
_fileSystem.GetFileSystemEntries(path)
.Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
.OrderBy(i => i.FullName);
return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
}
/// <summary>
/// Validates path.
/// </summary>
/// <param name="validatePathDto">Validate request object.</param>
/// <response code="200">Path validated.</response>
/// <response code="404">Path not found.</response>
/// <returns>Validation status.</returns>
[HttpPost("ValidatePath")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult ValidatePath([FromBody, BindRequired] ValidatePathDto validatePathDto)
{
if (validatePathDto.IsFile.HasValue)
{
if (validatePathDto.IsFile.Value)
{
if (!System.IO.File.Exists(validatePathDto.Path))
{
return NotFound();
}
}
else
{
if (!Directory.Exists(validatePathDto.Path))
{
return NotFound();
}
}
}
else
{
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
{
return NotFound();
}
if (validatePathDto.ValidateWritable)
{
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
try
{
System.IO.File.WriteAllText(file, string.Empty);
}
finally
{
if (System.IO.File.Exists(file))
{
System.IO.File.Delete(file);
}
}
}
}
return Ok();
}
/// <summary>
/// Gets network paths.
/// </summary>
/// <response code="200">Empty array returned.</response>
/// <returns>List of entries.</returns>
[Obsolete("This endpoint is obsolete.")]
[HttpGet("NetworkShares")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
{
_logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
return Array.Empty<FileSystemEntryInfo>();
}
/// <summary>
/// Gets available drives from the server's file system.
/// </summary>
/// <response code="200">List of entries returned.</response>
/// <returns>List of entries.</returns>
[HttpGet("Drives")]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<FileSystemEntryInfo> GetDrives()
{
return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
}
/// <summary>
/// Gets the parent path of a given path.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>Parent path.</returns>
[HttpGet("ParentPath")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<string?> GetParentPath([FromQuery, BindRequired] string path)
{
string? parent = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(parent))
{
// Check if unc share
var index = path.LastIndexOf(UncSeparator);
if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
{
parent = path.Substring(0, index);
if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
{
parent = null;
}
}
}
return parent;
}
/// <summary>
/// Get Default directory browser.
/// </summary>
/// <response code="200">Default directory browser returned.</response>
/// <returns>Default directory browser.</returns>
[HttpGet("DefaultDirectoryBrowser")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
{
return new DefaultDirectoryBrowserInfoDto();
}
}
}

View File

@@ -125,7 +125,6 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">Optional. User id.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="mediaTypes">[Unused] Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="isAiring">Optional. Is item airing.</param>
/// <param name="isMovie">Optional. Is item movie.</param>
/// <param name="isSports">Optional. Is item sports.</param>
@@ -137,12 +136,10 @@ namespace Jellyfin.Api.Controllers
/// <returns>Query filters.</returns>
[HttpGet("/Items/Filters2")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "mediaTypes", Justification = "Imported from ServiceStack")]
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
[FromQuery] string? parentId,
[FromQuery] string? includeItemTypes,
[FromQuery] string? mediaTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,

View File

@@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type)
public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string? name, [FromRoute] string? type)
{
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
? "folder"
@@ -110,8 +110,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetRatingImage(
[FromRoute] string theme,
[FromRoute] string name)
[FromRoute] string? theme,
[FromRoute] string? name)
{
return GetImageFile(_applicationPaths.RatingsPath, theme, name);
}
@@ -143,8 +143,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetMediaInfoImage(
[FromRoute] string theme,
[FromRoute] string name)
[FromRoute] string? theme,
[FromRoute] string? name)
{
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
}
@@ -156,7 +156,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="theme">Theme to search.</param>
/// <param name="name">File name to search for.</param>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name)
private ActionResult<FileStreamResult> GetImageFile(string basePath, string? theme, string? name)
{
var themeFolder = Path.Combine(basePath, theme);
if (Directory.Exists(themeFolder))

View File

@@ -0,0 +1,314 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The instant mix controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class InstantMixController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
private readonly ILibraryManager _libraryManager;
private readonly IMusicManager _musicManager;
/// <summary>
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public InstantMixController(
IUserManager userManager,
IDtoService dtoService,
IMusicManager musicManager,
ILibraryManager libraryManager)
{
_userManager = userManager;
_dtoService = dtoService;
_musicManager = musicManager;
_libraryManager = libraryManager;
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/Songs/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/Albums/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var album = _libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/Playlists/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="name">The genre name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/MusicGenres/{name}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
[FromRoute] string? name,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/Artists/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/MusicGenres/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
/// <summary>
/// Creates an instant playlist based on a given song.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <response code="200">Instant playlist returned.</response>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
[HttpGet("/Items/{id}/InstantMix")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
[FromRoute] Guid id,
[FromQuery] Guid userId,
[FromQuery] int? limit,
[FromQuery] string? fields,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes)
{
var item = _libraryManager.GetItemById(id);
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
return GetResult(items, user, limit, dtoOptions);
}
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User user, int? limit, DtoOptions dtoOptions)
{
var list = items;
var result = new QueryResult<BaseItemDto>
{
TotalRecordCount = list.Count
};
if (limit.HasValue)
{
list = list.Take(limit.Value).ToList();
}
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
result.Items = returnList;
return result;
}
}
}

View File

@@ -47,7 +47,6 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
/// <response code="204">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
@@ -55,14 +54,12 @@ namespace Jellyfin.Api.Controllers
[Description("Refreshes metadata for an item.")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "recursive", Justification = "Imported from ServiceStack")]
public ActionResult Post(
[FromRoute] Guid itemId,
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false,
[FromQuery] bool recursive = false)
[FromQuery] bool replaceAllImages = false)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)

View File

@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Items/{itemId}/ContentType")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string contentType)
public ActionResult UpdateItemContentType([FromRoute] Guid itemId, [FromQuery, BindRequired] string? contentType)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)

File diff suppressed because it is too large Load Diff

View File

@@ -50,13 +50,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets all virtual folders.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Virtual folders retrieved.</response>
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders()
{
return _libraryManager.GetVirtualFolders(true);
}
@@ -74,8 +72,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string name,
[FromQuery] string collectionType,
[FromQuery] string? name,
[FromQuery] string? collectionType,
[FromQuery] bool refreshLibrary,
[FromQuery] string[] paths,
[FromQuery] LibraryOptions libraryOptions)
@@ -102,7 +100,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveVirtualFolder(
[FromQuery] string name,
[FromQuery] string? name,
[FromQuery] bool refreshLibrary)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
@@ -125,8 +123,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public ActionResult RenameVirtualFolder(
[FromQuery] string name,
[FromQuery] string newName,
[FromQuery] string? name,
[FromQuery] string? newName,
[FromQuery] bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
@@ -207,8 +205,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddMediaPath(
[FromQuery] string name,
[FromQuery] string path,
[FromQuery] string? name,
[FromQuery] string? path,
[FromQuery] MediaPathInfo pathInfo,
[FromQuery] bool refreshLibrary)
{
@@ -258,7 +256,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaPath(
[FromQuery] string name,
[FromQuery] string? name,
[FromQuery] MediaPathInfo pathInfo)
{
if (string.IsNullOrWhiteSpace(name))
@@ -282,8 +280,8 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
[FromQuery] string name,
[FromQuery] string path,
[FromQuery] string? name,
[FromQuery] string? path,
[FromQuery] bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
@@ -329,7 +327,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateLibraryOptions(
[FromQuery] string id,
[FromQuery] string? id,
[FromQuery] LibraryOptions libraryOptions)
{
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,773 @@
using System;
using System.Buffers;
using System.Globalization;
using System.Linq;
using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The media info controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class MediaInfoController : BaseJellyfinApiController
{
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IDeviceManager _deviceManager;
private readonly ILibraryManager _libraryManager;
private readonly INetworkManager _networkManager;
private readonly IMediaEncoder _mediaEncoder;
private readonly IUserManager _userManager;
private readonly IAuthorizationContext _authContext;
private readonly ILogger _logger;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="MediaInfoController"/> class.
/// </summary>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="deviceManager">Instance of the <see cref="IDeviceManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{MediaInfoController}"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public MediaInfoController(
IMediaSourceManager mediaSourceManager,
IDeviceManager deviceManager,
ILibraryManager libraryManager,
INetworkManager networkManager,
IMediaEncoder mediaEncoder,
IUserManager userManager,
IAuthorizationContext authContext,
ILogger<MediaInfoController> logger,
IServerConfigurationManager serverConfigurationManager)
{
_mediaSourceManager = mediaSourceManager;
_deviceManager = deviceManager;
_libraryManager = libraryManager;
_networkManager = networkManager;
_mediaEncoder = mediaEncoder;
_userManager = userManager;
_authContext = authContext;
_logger = logger;
_serverConfigurationManager = serverConfigurationManager;
}
/// <summary>
/// Gets live playback media info for an item.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <response code="200">Playback info returned.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
[HttpGet("/Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute] Guid itemId, [FromQuery] Guid userId)
{
return await GetPlaybackInfoInternal(itemId, userId, null, null).ConfigureAwait(false);
}
/// <summary>
/// Gets live playback media info for an item.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="userId">The user id.</param>
/// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
/// <param name="startTimeTicks">The start time in ticks.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="maxAudioChannels">The maximum number of audio channels.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="liveStreamId">The livestream id.</param>
/// <param name="deviceProfile">The device profile.</param>
/// <param name="autoOpenLiveStream">Whether to auto open the livestream.</param>
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
/// <param name="enableTranscoding">Whether to enable transcoding. Default: true.</param>
/// <param name="allowVideoStreamCopy">Whether to allow to copy the video stream. Default: true.</param>
/// <param name="allowAudioStreamCopy">Whether to allow to copy the audio stream. Default: true.</param>
/// <response code="200">Playback info returned.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback info.</returns>
[HttpPost("/Items/{itemId}/PlaybackInfo")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
[FromRoute] Guid itemId,
[FromQuery] Guid userId,
[FromQuery] long? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] string mediaSourceId,
[FromQuery] string liveStreamId,
[FromQuery] DeviceProfile deviceProfile,
[FromQuery] bool autoOpenLiveStream,
[FromQuery] bool enableDirectPlay = true,
[FromQuery] bool enableDirectStream = true,
[FromQuery] bool enableTranscoding = true,
[FromQuery] bool allowVideoStreamCopy = true,
[FromQuery] bool allowAudioStreamCopy = true)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var profile = deviceProfile;
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
if (profile == null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
profile = caps.DeviceProfile;
}
}
var info = await GetPlaybackInfoInternal(itemId, userId, mediaSourceId, liveStreamId).ConfigureAwait(false);
if (profile != null)
{
// set device specific data
var item = _libraryManager.GetItemById(itemId);
foreach (var mediaSource in info.MediaSources)
{
SetDeviceSpecificData(
item,
mediaSource,
profile,
authInfo,
maxStreamingBitrate ?? profile.MaxStreamingBitrate,
startTimeTicks ?? 0,
mediaSourceId,
audioStreamIndex,
subtitleStreamIndex,
maxAudioChannels,
info!.PlaySessionId!,
userId,
enableDirectPlay,
enableDirectStream,
enableTranscoding,
allowVideoStreamCopy,
allowAudioStreamCopy);
}
SortMediaSources(info, maxStreamingBitrate);
}
if (autoOpenLiveStream)
{
var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
if (mediaSource != null && mediaSource.RequiresOpening && string.IsNullOrWhiteSpace(mediaSource.LiveStreamId))
{
var openStreamResult = await OpenMediaSource(new LiveStreamRequest
{
AudioStreamIndex = audioStreamIndex,
DeviceProfile = deviceProfile,
EnableDirectPlay = enableDirectPlay,
EnableDirectStream = enableDirectStream,
ItemId = itemId,
MaxAudioChannels = maxAudioChannels,
MaxStreamingBitrate = maxStreamingBitrate,
PlaySessionId = info.PlaySessionId,
StartTimeTicks = startTimeTicks,
SubtitleStreamIndex = subtitleStreamIndex,
UserId = userId,
OpenToken = mediaSource.OpenToken
}).ConfigureAwait(false);
info.MediaSources = new[] { openStreamResult.MediaSource };
}
}
if (info.MediaSources != null)
{
foreach (var mediaSource in info.MediaSources)
{
NormalizeMediaSourceContainer(mediaSource, profile!, DlnaProfileType.Video);
}
}
return info;
}
/// <summary>
/// Opens a media source.
/// </summary>
/// <param name="openToken">The open token.</param>
/// <param name="userId">The user id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
/// <param name="startTimeTicks">The start time in ticks.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="maxAudioChannels">The maximum number of audio channels.</param>
/// <param name="itemId">The item id.</param>
/// <param name="deviceProfile">The device profile.</param>
/// <param name="directPlayProtocols">The direct play protocols. Default: <see cref="MediaProtocol.Http"/>.</param>
/// <param name="enableDirectPlay">Whether to enable direct play. Default: true.</param>
/// <param name="enableDirectStream">Whether to enable direct stream. Default: true.</param>
/// <response code="200">Media source opened.</response>
/// <returns>A <see cref="Task"/> containing a <see cref="LiveStreamResponse"/>.</returns>
[HttpPost("/LiveStreams/Open")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<LiveStreamResponse>> OpenLiveStream(
[FromQuery] string openToken,
[FromQuery] Guid userId,
[FromQuery] string playSessionId,
[FromQuery] long? maxStreamingBitrate,
[FromQuery] long? startTimeTicks,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? maxAudioChannels,
[FromQuery] Guid itemId,
[FromQuery] DeviceProfile deviceProfile,
[FromQuery] MediaProtocol[] directPlayProtocols,
[FromQuery] bool enableDirectPlay = true,
[FromQuery] bool enableDirectStream = true)
{
var request = new LiveStreamRequest
{
OpenToken = openToken,
UserId = userId,
PlaySessionId = playSessionId,
MaxStreamingBitrate = maxStreamingBitrate,
StartTimeTicks = startTimeTicks,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
MaxAudioChannels = maxAudioChannels,
ItemId = itemId,
DeviceProfile = deviceProfile,
EnableDirectPlay = enableDirectPlay,
EnableDirectStream = enableDirectStream,
DirectPlayProtocols = directPlayProtocols ?? new[] { MediaProtocol.Http }
};
return await OpenMediaSource(request).ConfigureAwait(false);
}
/// <summary>
/// Closes a media source.
/// </summary>
/// <param name="liveStreamId">The livestream id.</param>
/// <response code="204">Livestream closed.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("/LiveStreams/Close")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CloseLiveStream([FromQuery] string liveStreamId)
{
_mediaSourceManager.CloseLiveStream(liveStreamId).GetAwaiter().GetResult();
return NoContent();
}
/// <summary>
/// Tests the network with a request with the size of the bitrate.
/// </summary>
/// <param name="size">The bitrate. Defaults to 102400.</param>
/// <response code="200">Test buffer returned.</response>
/// <response code="400">Size has to be a numer between 0 and 10,000,000.</response>
/// <returns>A <see cref="FileResult"/> with specified bitrate.</returns>
[HttpGet("/Playback/BitrateTest")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[Produces(MediaTypeNames.Application.Octet)]
public ActionResult GetBitrateTestBytes([FromQuery] int size = 102400)
{
const int MaxSize = 10_000_000;
if (size <= 0)
{
return BadRequest($"The requested size ({size}) is equal to or smaller than 0.");
}
if (size > MaxSize)
{
return BadRequest($"The requested size ({size}) is larger than the max allowed value ({MaxSize}).");
}
byte[] buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
new Random().NextBytes(buffer);
return File(buffer, MediaTypeNames.Application.Octet);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private async Task<PlaybackInfoResponse> GetPlaybackInfoInternal(
Guid id,
Guid userId,
string? mediaSourceId = null,
string? liveStreamId = null)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetItemById(id);
var result = new PlaybackInfoResponse();
MediaSourceInfo[] mediaSources;
if (string.IsNullOrWhiteSpace(liveStreamId))
{
// TODO (moved from MediaBrowser.Api) handle supportedLiveMediaTypes?
var mediaSourcesList = await _mediaSourceManager.GetPlaybackMediaSources(item, user, true, true, CancellationToken.None).ConfigureAwait(false);
if (string.IsNullOrWhiteSpace(mediaSourceId))
{
mediaSources = mediaSourcesList.ToArray();
}
else
{
mediaSources = mediaSourcesList
.Where(i => string.Equals(i.Id, mediaSourceId, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
}
else
{
var mediaSource = await _mediaSourceManager.GetLiveStream(liveStreamId, CancellationToken.None).ConfigureAwait(false);
mediaSources = new[] { mediaSource };
}
if (mediaSources.Length == 0)
{
result.MediaSources = Array.Empty<MediaSourceInfo>();
result.ErrorCode ??= PlaybackErrorCode.NoCompatibleStream;
}
else
{
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
// Should we move this directly into MediaSourceManager?
result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
}
return result;
}
private void NormalizeMediaSourceContainer(MediaSourceInfo mediaSource, DeviceProfile profile, DlnaProfileType type)
{
mediaSource.Container = StreamBuilder.NormalizeMediaSourceFormatIntoSingleContainer(mediaSource.Container, mediaSource.Path, profile, type);
}
private void SetDeviceSpecificData(
BaseItem item,
MediaSourceInfo mediaSource,
DeviceProfile profile,
AuthorizationInfo auth,
long? maxBitrate,
long startTimeTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex,
int? maxAudioChannels,
string playSessionId,
Guid userId,
bool enableDirectPlay,
bool enableDirectStream,
bool enableTranscoding,
bool allowVideoStreamCopy,
bool allowAudioStreamCopy)
{
var streamBuilder = new StreamBuilder(_mediaEncoder, _logger);
var options = new VideoOptions
{
MediaSources = new[] { mediaSource },
Context = EncodingContext.Streaming,
DeviceId = auth.DeviceId,
ItemId = item.Id,
Profile = profile,
MaxAudioChannels = maxAudioChannels
};
if (string.Equals(mediaSourceId, mediaSource.Id, StringComparison.OrdinalIgnoreCase))
{
options.MediaSourceId = mediaSourceId;
options.AudioStreamIndex = audioStreamIndex;
options.SubtitleStreamIndex = subtitleStreamIndex;
}
var user = _userManager.GetUserById(userId);
if (!enableDirectPlay)
{
mediaSource.SupportsDirectPlay = false;
}
if (!enableDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (!enableTranscoding)
{
mediaSource.SupportsTranscoding = false;
}
if (item is Audio)
{
_logger.LogInformation(
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
user.Username,
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
else
{
_logger.LogInformation(
"User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
user.Username,
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
// Beginning of Playback Determination: Attempt DirectPlay first
if (mediaSource.SupportsDirectPlay)
{
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectPlay = false;
}
else
{
var supportsDirectStream = mediaSource.SupportsDirectStream;
// Dummy this up to fool StreamBuilder
mediaSource.SupportsDirectStream = true;
options.MaxBitrate = maxBitrate;
if (item is Audio)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectPlay = true;
}
}
else if (item is Video)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectPlay = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectPlay = false;
}
// Set this back to what it was
mediaSource.SupportsDirectStream = supportsDirectStream;
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
if (mediaSource.SupportsDirectStream)
{
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectStream = false;
}
else
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
if (item is Audio)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectStream = true;
}
}
else if (item is Video)
{
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
}
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (streamInfo == null || !streamInfo.IsDirectStream)
{
mediaSource.SupportsDirectStream = false;
}
if (streamInfo != null)
{
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
if (mediaSource.SupportsTranscoding)
{
options.MaxBitrate = GetMaxBitrate(maxBitrate, user);
// The MediaSource supports direct stream, now test to see if the client supports it
var streamInfo = string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
if (streamInfo != null)
{
streamInfo.PlaySessionId = playSessionId;
streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
else
{
if (streamInfo != null)
{
streamInfo.PlaySessionId = playSessionId;
if (streamInfo.PlayMethod == PlayMethod.Transcode)
{
streamInfo.StartPositionTicks = startTimeTicks;
mediaSource.TranscodingUrl = streamInfo.ToUrl("-", auth.Token).TrimStart('-');
if (!allowVideoStreamCopy)
{
mediaSource.TranscodingUrl += "&allowVideoStreamCopy=false";
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
}
if (!allowAudioStreamCopy)
{
mediaSource.TranscodingUrl += "&allowAudioStreamCopy=false";
}
mediaSource.TranscodingContainer = streamInfo.Container;
mediaSource.TranscodingSubProtocol = streamInfo.SubProtocol;
// Do this after the above so that StartPositionTicks is set
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
}
}
}
foreach (var attachment in mediaSource.MediaAttachments)
{
attachment.DeliveryUrl = string.Format(
CultureInfo.InvariantCulture,
"/Videos/{0}/{1}/Attachments/{2}",
item.Id,
mediaSource.Id,
attachment.Index);
}
}
private async Task<LiveStreamResponse> OpenMediaSource(LiveStreamRequest request)
{
var authInfo = _authContext.GetAuthorizationInfo(Request);
var result = await _mediaSourceManager.OpenLiveStream(request, CancellationToken.None).ConfigureAwait(false);
var profile = request.DeviceProfile;
if (profile == null)
{
var caps = _deviceManager.GetCapabilities(authInfo.DeviceId);
if (caps != null)
{
profile = caps.DeviceProfile;
}
}
if (profile != null)
{
var item = _libraryManager.GetItemById(request.ItemId);
SetDeviceSpecificData(
item,
result.MediaSource,
profile,
authInfo,
request.MaxStreamingBitrate,
request.StartTimeTicks ?? 0,
result.MediaSource.Id,
request.AudioStreamIndex,
request.SubtitleStreamIndex,
request.MaxAudioChannels,
request.PlaySessionId,
request.UserId,
request.EnableDirectPlay,
request.EnableDirectStream,
true,
true,
true);
}
else
{
if (!string.IsNullOrWhiteSpace(result.MediaSource.TranscodingUrl))
{
result.MediaSource.TranscodingUrl += "&LiveStreamId=" + result.MediaSource.LiveStreamId;
}
}
// here was a check if (result.MediaSource != null) but Rider said it will never be null
NormalizeMediaSourceContainer(result.MediaSource, profile!, DlnaProfileType.Video);
return result;
}
private void SetDeviceSpecificSubtitleInfo(StreamInfo info, MediaSourceInfo mediaSource, string accessToken)
{
var profiles = info.GetSubtitleProfiles(_mediaEncoder, false, "-", accessToken);
mediaSource.DefaultSubtitleStreamIndex = info.SubtitleStreamIndex;
mediaSource.TranscodeReasons = info.TranscodeReasons;
foreach (var profile in profiles)
{
foreach (var stream in mediaSource.MediaStreams)
{
if (stream.Type == MediaStreamType.Subtitle && stream.Index == profile.Index)
{
stream.DeliveryMethod = profile.DeliveryMethod;
if (profile.DeliveryMethod == SubtitleDeliveryMethod.External)
{
stream.DeliveryUrl = profile.Url.TrimStart('-');
stream.IsExternalUrl = profile.IsExternalUrl;
}
}
}
}
}
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{
remoteClientMaxBitrate = _serverConfigurationManager.Configuration.RemoteClientBitrateLimit;
}
if (remoteClientMaxBitrate > 0)
{
var isInLocalNetwork = _networkManager.IsInLocalNetwork(Request.HttpContext.Connection.RemoteIpAddress.ToString());
_logger.LogInformation("RemoteClientBitrateLimit: {0}, RemoteIp: {1}, IsInLocalNetwork: {2}", remoteClientMaxBitrate, Request.HttpContext.Connection.RemoteIpAddress.ToString(), isInLocalNetwork);
if (!isInLocalNetwork)
{
maxBitrate = Math.Min(maxBitrate ?? remoteClientMaxBitrate, remoteClientMaxBitrate);
}
}
return maxBitrate;
}
private void SortMediaSources(PlaybackInfoResponse result, long? maxBitrate)
{
var originalList = result.MediaSources.ToList();
result.MediaSources = result.MediaSources.OrderBy(i =>
{
// Nothing beats direct playing a file
if (i.SupportsDirectPlay && i.Protocol == MediaProtocol.File)
{
return 0;
}
return 1;
})
.ThenBy(i =>
{
// Let's assume direct streaming a file is just as desirable as direct playing a remote url
if (i.SupportsDirectPlay || i.SupportsDirectStream)
{
return 0;
}
return 1;
})
.ThenBy(i =>
{
return i.Protocol switch
{
MediaProtocol.File => 0,
_ => 1,
};
})
.ThenBy(i =>
{
if (maxBitrate.HasValue && i.Bitrate.HasValue)
{
return i.Bitrate.Value <= maxBitrate.Value ? 0 : 2;
}
return 1;
})
.ThenBy(originalList.IndexOf)
.ToArray();
}
}
}

View File

@@ -0,0 +1,340 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Movies controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class MoviesController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="MoviesController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public MoviesController(
IUserManager userManager,
ILibraryManager libraryManager,
IDtoService dtoService,
IServerConfigurationManager serverConfigurationManager)
{
_userManager = userManager;
_libraryManager = libraryManager;
_dtoService = dtoService;
_serverConfigurationManager = serverConfigurationManager;
}
/// <summary>
/// Gets movie recommendations.
/// </summary>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="enableImages">(Unused) Optional. include image information in output.</param>
/// <param name="enableUserData">(Unused) Optional. include user data.</param>
/// <param name="imageTypeLimit">(Unused) Optional. the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">(Unused) Optional. The image types to include in the output.</param>
/// <param name="fields">Optional. The fields to return.</param>
/// <param name="categoryLimit">The max number of categories to return.</param>
/// <param name="itemLimit">The max number of items to return per category.</param>
/// <response code="200">Movie recommendations returned.</response>
/// <returns>The list of movie recommendations.</returns>
[HttpGet("Recommendations")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImages", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableUserData", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageTypeLimit", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "enableImageTypes", Justification = "Imported from ServiceStack")]
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
[FromQuery] Guid userId,
[FromQuery] string parentId,
[FromQuery] bool? enableImages,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] string? fields,
[FromQuery] int categoryLimit = 5,
[FromQuery] int itemLimit = 8)
{
var user = _userManager.GetUserById(userId);
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request);
var categories = new List<RecommendationDto>();
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
var query = new InternalItemsQuery(user)
{
IncludeItemTypes = new[]
{
nameof(Movie),
// typeof(Trailer).Name,
// typeof(LiveTvProgram).Name
},
// IsMovie = true
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
Limit = 7,
ParentId = parentIdGuid,
Recursive = true,
IsPlayed = true,
DtoOptions = dtoOptions
};
var recentlyPlayedMovies = _libraryManager.GetItemList(query);
var itemTypes = new List<string> { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
var likedMovies = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
Limit = 10,
IsFavoriteOrLiked = true,
ExcludeItemIds = recentlyPlayedMovies.Select(i => i.Id).ToArray(),
EnableGroupByMetadataKey = true,
ParentId = parentIdGuid,
Recursive = true,
DtoOptions = dtoOptions
});
var mostRecentMovies = recentlyPlayedMovies.Take(6).ToList();
// Get recently played directors
var recentDirectors = GetDirectors(mostRecentMovies)
.ToList();
// Get recently played actors
var recentActors = GetActors(mostRecentMovies)
.ToList();
var similarToRecentlyPlayed = GetSimilarTo(user, recentlyPlayedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToRecentlyPlayed).GetEnumerator();
var similarToLiked = GetSimilarTo(user, likedMovies, itemLimit, dtoOptions, RecommendationType.SimilarToLikedItem).GetEnumerator();
var hasDirectorFromRecentlyPlayed = GetWithDirector(user, recentDirectors, itemLimit, dtoOptions, RecommendationType.HasDirectorFromRecentlyPlayed).GetEnumerator();
var hasActorFromRecentlyPlayed = GetWithActor(user, recentActors, itemLimit, dtoOptions, RecommendationType.HasActorFromRecentlyPlayed).GetEnumerator();
var categoryTypes = new List<IEnumerator<RecommendationDto>>
{
// Give this extra weight
similarToRecentlyPlayed,
similarToRecentlyPlayed,
// Give this extra weight
similarToLiked,
similarToLiked,
hasDirectorFromRecentlyPlayed,
hasActorFromRecentlyPlayed
};
while (categories.Count < categoryLimit)
{
var allEmpty = true;
foreach (var category in categoryTypes)
{
if (category.MoveNext())
{
categories.Add(category.Current);
allEmpty = false;
if (categories.Count >= categoryLimit)
{
break;
}
}
}
if (allEmpty)
{
break;
}
}
return Ok(categories.OrderBy(i => i.RecommendationType));
}
private IEnumerable<RecommendationDto> GetWithDirector(
User user,
IEnumerable<string> names,
int itemLimit,
DtoOptions dtoOptions,
RecommendationType type)
{
var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
foreach (var name in names)
{
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
Person = name,
// Account for duplicates by imdb id, since the database doesn't support this yet
Limit = itemLimit + 2,
PersonTypes = new[] { PersonType.Director },
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
}).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
.Select(x => x.First())
.Take(itemLimit)
.ToList();
if (items.Count > 0)
{
var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
yield return new RecommendationDto
{
BaselineItemName = name,
CategoryId = name.GetMD5(),
RecommendationType = type,
Items = returnItems
};
}
}
}
private IEnumerable<RecommendationDto> GetWithActor(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
var itemTypes = new List<string> { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
foreach (var name in names)
{
var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
Person = name,
// Account for duplicates by imdb id, since the database doesn't support this yet
Limit = itemLimit + 2,
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
}).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
.Select(x => x.First())
.Take(itemLimit)
.ToList();
if (items.Count > 0)
{
var returnItems = _dtoService.GetBaseItemDtos(items, dtoOptions, user);
yield return new RecommendationDto
{
BaselineItemName = name,
CategoryId = name.GetMD5(),
RecommendationType = type,
Items = returnItems
};
}
}
}
private IEnumerable<RecommendationDto> GetSimilarTo(User user, IEnumerable<BaseItem> baselineItems, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
{
var itemTypes = new List<string> { nameof(Movie) };
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
{
itemTypes.Add(nameof(Trailer));
itemTypes.Add(nameof(LiveTvProgram));
}
foreach (var item in baselineItems)
{
var similar = _libraryManager.GetItemList(new InternalItemsQuery(user)
{
Limit = itemLimit,
IncludeItemTypes = itemTypes.ToArray(),
IsMovie = true,
SimilarTo = item,
EnableGroupByMetadataKey = true,
DtoOptions = dtoOptions
});
if (similar.Count > 0)
{
var returnItems = _dtoService.GetBaseItemDtos(similar, dtoOptions, user);
yield return new RecommendationDto
{
BaselineItemName = item.Name,
CategoryId = item.Id,
RecommendationType = type,
Items = returnItems
};
}
}
}
private IEnumerable<string> GetActors(IEnumerable<BaseItem> items)
{
var people = _libraryManager.GetPeople(new InternalPeopleQuery
{
ExcludePersonTypes = new[] { PersonType.Director },
MaxListOrder = 3
});
var itemIds = items.Select(i => i.Id).ToList();
return people
.Where(i => itemIds.Contains(i.ItemId))
.Select(i => i.Name)
.DistinctNames();
}
private IEnumerable<string> GetDirectors(IEnumerable<BaseItem> items)
{
var people = _libraryManager.GetPeople(new InternalPeopleQuery
{
PersonTypes = new[] { PersonType.Director }
});
var itemIds = items.Select(i => i.Id).ToList();
return people
.Where(i => itemIds.Contains(i.ItemId))
.Select(i => i.Name)
.DistinctNames();
}
}
}

View File

@@ -36,23 +36,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets a user's notifications.
/// </summary>
/// <param name="userId">The user's ID.</param>
/// <param name="isRead">An optional filter by notification read state.</param>
/// <param name="startIndex">The optional index to start at. All notifications with a lower index will be omitted from the results.</param>
/// <param name="limit">An optional limit on the number of notifications returned.</param>
/// <response code="200">Notifications returned.</response>
/// <returns>An <see cref="OkResult"/> containing a list of notifications.</returns>
[HttpGet("{userId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isRead", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "startIndex", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "limit", Justification = "Imported from ServiceStack")]
public ActionResult<NotificationResultDto> GetNotifications(
[FromRoute] string userId,
[FromQuery] bool? isRead,
[FromQuery] int? startIndex,
[FromQuery] int? limit)
public ActionResult<NotificationResultDto> GetNotifications()
{
return new NotificationResultDto();
}
@@ -60,14 +48,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets a user's notification summary.
/// </summary>
/// <param name="userId">The user's ID.</param>
/// <response code="200">Summary of user's notifications returned.</response>
/// <returns>An <cref see="OkResult"/> containing a summary of the users notifications.</returns>
[HttpGet("{userId}/Summary")]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
public ActionResult<NotificationsSummaryDto> GetNotificationsSummary(
[FromRoute] string userId)
public ActionResult<NotificationsSummaryDto> GetNotificationsSummary()
{
return new NotificationsSummaryDto();
}
@@ -108,8 +93,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateAdminNotification(
[FromQuery] string name,
[FromQuery] string description,
[FromQuery] string? name,
[FromQuery] string? description,
[FromQuery] string? url,
[FromQuery] NotificationLevel? level)
{
@@ -134,17 +119,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sets notifications as read.
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
/// <response code="204">Notifications set as read.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("{userId}/Read")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
public ActionResult SetRead(
[FromRoute] string userId,
[FromQuery] string ids)
public ActionResult SetRead()
{
return NoContent();
}
@@ -152,17 +131,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Sets notifications as unread.
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
/// <response code="204">Notifications set as unread.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("{userId}/Unread")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Imported from ServiceStack")]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "ids", Justification = "Imported from ServiceStack")]
public ActionResult SetUnread(
[FromRoute] string userId,
[FromQuery] string ids)
public ActionResult SetUnread()
{
return NoContent();
}

View File

@@ -40,7 +40,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PackageInfo>> GetPackageInfo(
[FromRoute] [Required] string name,
[FromRoute] [Required] string? name,
[FromQuery] string? assemblyGuid)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
@@ -80,9 +80,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
[FromRoute] [Required] string name,
[FromQuery] string assemblyGuid,
[FromQuery] string version)
[FromRoute] [Required] string? name,
[FromQuery] string? assemblyGuid,
[FromQuery] string? version)
{
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
var package = _installationManager.GetCompatibleVersions(

View File

@@ -0,0 +1,282 @@
using System;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Persons controller.
/// </summary>
public class PersonsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="PersonsController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public PersonsController(
ILibraryManager libraryManager,
IDtoService dtoService,
IUserManager userManager)
{
_libraryManager = libraryManager;
_dtoService = dtoService;
_userManager = userManager;
}
/// <summary>
/// Gets all persons from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person id.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
/// <response code="200">Persons returned.</response>
/// <returns>An <see cref="OkResult"/> containing the queryresult of persons.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetPersons(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string searchTerm,
[FromQuery] string parentId,
[FromQuery] string fields,
[FromQuery] string excludeItemTypes,
[FromQuery] string includeItemTypes,
[FromQuery] string filters,
[FromQuery] bool? isFavorite,
[FromQuery] string mediaTypes,
[FromQuery] string genres,
[FromQuery] string genreIds,
[FromQuery] string officialRatings,
[FromQuery] string tags,
[FromQuery] string years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string enableImageTypes,
[FromQuery] string person,
[FromQuery] string personIds,
[FromQuery] string personTypes,
[FromQuery] string studios,
[FromQuery] string studioIds,
[FromQuery] Guid userId,
[FromQuery] string nameStartsWithOrGreater,
[FromQuery] string nameStartsWith,
[FromQuery] string nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (!userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true),
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
Genres = RequestHelpers.Split(genres, '|', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(y => Convert.ToInt32(y, CultureInfo.InvariantCulture)).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
if (!string.IsNullOrWhiteSpace(parentId))
{
if (parentItem is Folder)
{
query.AncestorIds = new[] { new Guid(parentId) };
}
else
{
query.ItemIds = new[] { new Guid(parentId) };
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|')
.Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null)
.Select(i => i!.Id)
.ToArray();
}
foreach (var filter in RequestHelpers.GetFilters(filters))
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, counts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = counts.ItemCount;
dto.ProgramCount = counts.ProgramCount;
dto.SeriesCount = counts.SeriesCount;
dto.EpisodeCount = counts.EpisodeCount;
dto.MovieCount = counts.MovieCount;
dto.TrailerCount = counts.TrailerCount;
dto.AlbumCount = counts.AlbumCount;
dto.SongCount = counts.SongCount;
dto.ArtistCount = counts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
}
/// <summary>
/// Get person by name.
/// </summary>
/// <param name="name">Person name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <response code="200">Person returned.</response>
/// <response code="404">Person not found.</response>
/// <returns>An <see cref="OkResult"/> containing the person on success,
/// or a <see cref="NotFoundResult"/> if person not found.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BaseItemDto> GetPerson([FromRoute] string name, [FromQuery] Guid userId)
{
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
var item = _libraryManager.GetPerson(name);
if (item == null)
{
return NotFound();
}
if (!userId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(userId);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
}

View File

@@ -84,8 +84,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddToPlaylist(
[FromRoute] string playlistId,
[FromQuery] string ids,
[FromRoute] string? playlistId,
[FromQuery] string? ids,
[FromQuery] Guid userId)
{
_playlistManager.AddToPlaylist(playlistId, RequestHelpers.GetGuids(ids), userId);
@@ -103,8 +103,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("{playlistId}/Items/{itemId}/Move/{newIndex}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult MoveItem(
[FromRoute] string playlistId,
[FromRoute] string itemId,
[FromRoute] string? playlistId,
[FromRoute] string? itemId,
[FromRoute] int newIndex)
{
_playlistManager.MoveItem(playlistId, itemId, newIndex);
@@ -120,7 +120,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveFromPlaylist([FromRoute] string playlistId, [FromQuery] string entryIds)
public ActionResult RemoveFromPlaylist([FromRoute] string? playlistId, [FromQuery] string? entryIds)
{
_playlistManager.RemoveFromPlaylist(playlistId, RequestHelpers.Split(entryIds, ',', true));
return NoContent();
@@ -147,11 +147,11 @@ namespace Jellyfin.Api.Controllers
[FromRoute] Guid userId,
[FromRoute] int? startIndex,
[FromRoute] int? limit,
[FromRoute] string fields,
[FromRoute] string? fields,
[FromRoute] bool? enableImages,
[FromRoute] bool? enableUserData,
[FromRoute] int? imageTypeLimit,
[FromRoute] string enableImageTypes)
[FromRoute] string? enableImageTypes)
{
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
if (playlist == null)

View File

@@ -0,0 +1,372 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Playstate controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PlaystateController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
private readonly ILibraryManager _libraryManager;
private readonly ISessionManager _sessionManager;
private readonly IAuthorizationContext _authContext;
private readonly ILogger<PlaystateController> _logger;
private readonly TranscodingJobHelper _transcodingJobHelper;
/// <summary>
/// Initializes a new instance of the <see cref="PlaystateController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public PlaystateController(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
ISessionManager sessionManager,
IAuthorizationContext authContext,
ILoggerFactory loggerFactory,
IMediaSourceManager mediaSourceManager,
IFileSystem fileSystem)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
_libraryManager = libraryManager;
_sessionManager = sessionManager;
_authContext = authContext;
_logger = loggerFactory.CreateLogger<PlaystateController>();
_transcodingJobHelper = new TranscodingJobHelper(
loggerFactory.CreateLogger<TranscodingJobHelper>(),
mediaSourceManager,
fileSystem);
}
/// <summary>
/// Marks an item as played for user.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="datePlayed">Optional. The date the item was played.</param>
/// <response code="200">Item marked as played.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("/Users/{userId}/PlayedItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkPlayedItem(
[FromRoute] Guid userId,
[FromRoute] Guid itemId,
[FromQuery] DateTime? datePlayed)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var dto = UpdatePlayedStatus(user, itemId, true, datePlayed);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
UpdatePlayedStatus(additionalUser, itemId, true, datePlayed);
}
return dto;
}
/// <summary>
/// Marks an item as unplayed for user.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as unplayed.</response>
/// <returns>A <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("/Users/{userId}/PlayedItem/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkUnplayedItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
var dto = UpdatePlayedStatus(user, itemId, false, null);
foreach (var additionalUserInfo in session.AdditionalUsers)
{
var additionalUser = _userManager.GetUserById(additionalUserInfo.UserId);
UpdatePlayedStatus(additionalUser, itemId, false, null);
}
return dto;
}
/// <summary>
/// Reports playback has started within a session.
/// </summary>
/// <param name="playbackStartInfo">The playback start info.</param>
/// <response code="204">Playback start recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Sessions/Playing")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ReportPlaybackStart([FromBody] PlaybackStartInfo playbackStartInfo)
{
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Reports playback progress within a session.
/// </summary>
/// <param name="playbackProgressInfo">The playback progress info.</param>
/// <response code="204">Playback progress recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Sessions/Playing/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ReportPlaybackProgress([FromBody] PlaybackProgressInfo playbackProgressInfo)
{
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Pings a playback session.
/// </summary>
/// <param name="playSessionId">Playback session id.</param>
/// <response code="204">Playback session pinged.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Sessions/Playing/Ping")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
{
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
return NoContent();
}
/// <summary>
/// Reports playback has stopped within a session.
/// </summary>
/// <param name="playbackStopInfo">The playback stop info.</param>
/// <response code="204">Playback stop recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Sessions/Playing/Stopped")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> ReportPlaybackStopped([FromBody] PlaybackStopInfo playbackStopInfo)
{
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Reports that a user has begun playing an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="canSeek">Indicates if the client can seek.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="playMethod">The play method.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <response code="204">Play start recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Users/{userId}/PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStart(
[FromRoute] Guid userId,
[FromRoute] Guid itemId,
[FromQuery] string mediaSourceId,
[FromQuery] bool canSeek,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] PlayMethod playMethod,
[FromQuery] string liveStreamId,
[FromQuery] string playSessionId)
{
var playbackStartInfo = new PlaybackStartInfo
{
CanSeek = canSeek,
ItemId = itemId,
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
PlayMethod = playMethod,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId
};
playbackStartInfo.PlayMethod = ValidatePlayMethod(playbackStartInfo.PlayMethod, playbackStartInfo.PlaySessionId);
playbackStartInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackStart(playbackStartInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Reports a user's playback progress.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="positionTicks">Optional. The current position, in ticks. 1 tick = 10000 ms.</param>
/// <param name="isPaused">Indicates if the player is paused.</param>
/// <param name="isMuted">Indicates if the player is muted.</param>
/// <param name="audioStreamIndex">The audio stream index.</param>
/// <param name="subtitleStreamIndex">The subtitle stream index.</param>
/// <param name="volumeLevel">Scale of 0-100.</param>
/// <param name="playMethod">The play method.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="repeatMode">The repeat mode.</param>
/// <response code="204">Play progress recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Users/{userId}/PlayingItems/{itemId}/Progress")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackProgress(
[FromRoute] Guid userId,
[FromRoute] Guid itemId,
[FromQuery] string mediaSourceId,
[FromQuery] long? positionTicks,
[FromQuery] bool isPaused,
[FromQuery] bool isMuted,
[FromQuery] int? audioStreamIndex,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] int? volumeLevel,
[FromQuery] PlayMethod playMethod,
[FromQuery] string liveStreamId,
[FromQuery] string playSessionId,
[FromQuery] RepeatMode repeatMode)
{
var playbackProgressInfo = new PlaybackProgressInfo
{
ItemId = itemId,
PositionTicks = positionTicks,
IsMuted = isMuted,
IsPaused = isPaused,
MediaSourceId = mediaSourceId,
AudioStreamIndex = audioStreamIndex,
SubtitleStreamIndex = subtitleStreamIndex,
VolumeLevel = volumeLevel,
PlayMethod = playMethod,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
RepeatMode = repeatMode
};
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
playbackProgressInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackProgress(playbackProgressInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Reports that a user has stopped playing an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="mediaSourceId">The id of the MediaSource.</param>
/// <param name="nextMediaType">The next media type that will play.</param>
/// <param name="positionTicks">Optional. The position, in ticks, where playback stopped. 1 tick = 10000 ms.</param>
/// <param name="liveStreamId">The live stream id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <response code="204">Playback stop recorded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("/Users/{userId}/PlayingItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "userId", Justification = "Required for ServiceStack")]
public async Task<ActionResult> OnPlaybackStopped(
[FromRoute] Guid userId,
[FromRoute] Guid itemId,
[FromQuery] string mediaSourceId,
[FromQuery] string nextMediaType,
[FromQuery] long? positionTicks,
[FromQuery] string liveStreamId,
[FromQuery] string playSessionId)
{
var playbackStopInfo = new PlaybackStopInfo
{
ItemId = itemId,
PositionTicks = positionTicks,
MediaSourceId = mediaSourceId,
PlaySessionId = playSessionId,
LiveStreamId = liveStreamId,
NextMediaType = nextMediaType
};
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId))
{
await _transcodingJobHelper.KillTranscodingJobs(_authContext.GetAuthorizationInfo(Request).DeviceId, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false);
}
playbackStopInfo.SessionId = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
await _sessionManager.OnPlaybackStopped(playbackStopInfo).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Updates the played status.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="itemId">The item id.</param>
/// <param name="wasPlayed">if set to <c>true</c> [was played].</param>
/// <param name="datePlayed">The date played.</param>
/// <returns>Task.</returns>
private UserItemDataDto UpdatePlayedStatus(User user, Guid itemId, bool wasPlayed, DateTime? datePlayed)
{
var item = _libraryManager.GetItemById(itemId);
if (wasPlayed)
{
item.MarkPlayed(user, datePlayed, true);
}
else
{
item.MarkUnplayed(user);
}
return _userDataRepository.GetUserDataDto(item, user);
}
private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
{
if (method == PlayMethod.Transcode)
{
var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId);
if (job == null)
{
return PlayMethod.DirectPlay;
}
}
return method;
}
}
}

View File

@@ -42,13 +42,11 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <param name="isAppStoreEnabled">Optional. Unused.</param>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isAppStoreEnabled", Justification = "Imported from ServiceStack")]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
public ActionResult<IEnumerable<PluginInfo>> GetPlugins()
{
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
}
@@ -168,7 +166,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name)
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string? name)
{
return new MBRegistrationRecord
{
@@ -190,7 +188,7 @@ namespace Jellyfin.Api.Controllers
[Obsolete("Paid plugins are not supported")]
[HttpGet("/Registrations/{name}")]
[ProducesResponseType(StatusCodes.Status501NotImplemented)]
public ActionResult GetRegistration([FromRoute] string name)
public ActionResult GetRegistration([FromRoute] string? name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.

View File

@@ -208,7 +208,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> DownloadRemoteImage(
[FromRoute] Guid itemId,
[FromQuery, BindRequired] ImageType type,
[FromQuery] string imageUrl)
[FromQuery] string? imageUrl)
{
var item = _libraryManager.GetItemById(itemId);
if (item == null)

View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Api.Constants;
using MediaBrowser.Model.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Scheduled Tasks Controller.
/// </summary>
[Authorize(Policy = Policies.RequiresElevation)]
public class ScheduledTasksController : BaseJellyfinApiController
{
private readonly ITaskManager _taskManager;
/// <summary>
/// Initializes a new instance of the <see cref="ScheduledTasksController"/> class.
/// </summary>
/// <param name="taskManager">Instance of the <see cref="ITaskManager"/> interface.</param>
public ScheduledTasksController(ITaskManager taskManager)
{
_taskManager = taskManager;
}
/// <summary>
/// Get tasks.
/// </summary>
/// <param name="isHidden">Optional filter tasks that are hidden, or not.</param>
/// <param name="isEnabled">Optional filter tasks that are enabled, or not.</param>
/// <response code="200">Scheduled tasks retrieved.</response>
/// <returns>The list of scheduled tasks.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public IEnumerable<IScheduledTaskWorker> GetTasks(
[FromQuery] bool? isHidden,
[FromQuery] bool? isEnabled)
{
IEnumerable<IScheduledTaskWorker> tasks = _taskManager.ScheduledTasks.OrderBy(o => o.Name);
foreach (var task in tasks)
{
if (task.ScheduledTask is IConfigurableScheduledTask scheduledTask)
{
if (isHidden.HasValue && isHidden.Value != scheduledTask.IsHidden)
{
continue;
}
if (isEnabled.HasValue && isEnabled.Value != scheduledTask.IsEnabled)
{
continue;
}
}
yield return task;
}
}
/// <summary>
/// Get task by id.
/// </summary>
/// <param name="taskId">Task Id.</param>
/// <response code="200">Task retrieved.</response>
/// <response code="404">Task not found.</response>
/// <returns>An <see cref="OkResult"/> containing the task on success, or a <see cref="NotFoundResult"/> if the task could not be found.</returns>
[HttpGet("{taskId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<TaskInfo> GetTask([FromRoute] string? taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(i =>
string.Equals(i.Id, taskId, StringComparison.OrdinalIgnoreCase));
if (task == null)
{
return NotFound();
}
return ScheduledTaskHelpers.GetTaskInfo(task);
}
/// <summary>
/// Start specified task.
/// </summary>
/// <param name="taskId">Task Id.</param>
/// <response code="204">Task started.</response>
/// <response code="404">Task not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpPost("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult StartTask([FromRoute] string? taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
if (task == null)
{
return NotFound();
}
_taskManager.Execute(task, new TaskOptions());
return NoContent();
}
/// <summary>
/// Stop specified task.
/// </summary>
/// <param name="taskId">Task Id.</param>
/// <response code="204">Task stopped.</response>
/// <response code="404">Task not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("Running/{taskId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult StopTask([FromRoute] string? taskId)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
if (task == null)
{
return NotFound();
}
_taskManager.Cancel(task);
return NoContent();
}
/// <summary>
/// Update specified task triggers.
/// </summary>
/// <param name="taskId">Task Id.</param>
/// <param name="triggerInfos">Triggers.</param>
/// <response code="204">Task triggers updated.</response>
/// <response code="404">Task not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpPost("{taskId}/Triggers")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateTask(
[FromRoute] string? taskId,
[FromBody, BindRequired] TaskTriggerInfo[] triggerInfos)
{
var task = _taskManager.ScheduledTasks.FirstOrDefault(o =>
o.Id.Equals(taskId, StringComparison.OrdinalIgnoreCase));
if (task == null)
{
return NotFound();
}
task.Triggers = triggerInfos;
return NoContent();
}
}
}

View File

@@ -81,11 +81,11 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] Guid userId,
[FromQuery, Required] string searchTerm,
[FromQuery] string includeItemTypes,
[FromQuery] string excludeItemTypes,
[FromQuery] string mediaTypes,
[FromQuery] string parentId,
[FromQuery, Required] string? searchTerm,
[FromQuery] string? includeItemTypes,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? mediaTypes,
[FromQuery] string? parentId,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSeries,
[FromQuery] bool? isNews,

View File

@@ -62,7 +62,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<SessionInfo>> GetSessions(
[FromQuery] Guid controllableByUserId,
[FromQuery] string deviceId,
[FromQuery] string? deviceId,
[FromQuery] int? activeWithinSeconds)
{
var result = _sessionManager.Sessions;
@@ -123,10 +123,10 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Viewing")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DisplayContent(
[FromRoute] string sessionId,
[FromQuery] string itemType,
[FromQuery] string itemId,
[FromQuery] string itemName)
[FromRoute] string? sessionId,
[FromQuery] string? itemType,
[FromQuery] string? itemId,
[FromQuery] string? itemName)
{
var command = new BrowseRequest
{
@@ -157,7 +157,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Playing")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult Play(
[FromRoute] string sessionId,
[FromRoute] string? sessionId,
[FromQuery] Guid[] itemIds,
[FromQuery] long? startPositionTicks,
[FromQuery] PlayCommand playCommand,
@@ -191,7 +191,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Playing/{command}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendPlaystateCommand(
[FromRoute] string sessionId,
[FromRoute] string? sessionId,
[FromBody] PlaystateRequest playstateRequest)
{
_sessionManager.SendPlaystateCommand(
@@ -213,8 +213,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/System/{command}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendSystemCommand(
[FromRoute] string sessionId,
[FromRoute] string command)
[FromRoute] string? sessionId,
[FromRoute] string? command)
{
var name = command;
if (Enum.TryParse(name, true, out GeneralCommandType commandType))
@@ -244,8 +244,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Command/{Command}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendGeneralCommand(
[FromRoute] string sessionId,
[FromRoute] string command)
[FromRoute] string? sessionId,
[FromRoute] string? command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -270,7 +270,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Command")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendFullGeneralCommand(
[FromRoute] string sessionId,
[FromRoute] string? sessionId,
[FromBody, Required] GeneralCommand command)
{
var currentSession = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
@@ -303,9 +303,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/Message")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SendMessageCommand(
[FromRoute] string sessionId,
[FromQuery] string text,
[FromQuery] string header,
[FromRoute] string? sessionId,
[FromQuery] string? text,
[FromQuery] string? header,
[FromQuery] long? timeoutMs)
{
var command = new MessageCommand
@@ -330,7 +330,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/{sessionId}/User/{userId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddUserToSession(
[FromRoute] string sessionId,
[FromRoute] string? sessionId,
[FromRoute] Guid userId)
{
_sessionManager.AddAdditionalUser(sessionId, userId);
@@ -347,7 +347,7 @@ namespace Jellyfin.Api.Controllers
[HttpDelete("/Sessions/{sessionId}/User/{userId}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveUserFromSession(
[FromRoute] string sessionId,
[FromRoute] string? sessionId,
[FromRoute] Guid userId)
{
_sessionManager.RemoveAdditionalUser(sessionId, userId);
@@ -368,9 +368,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/Capabilities")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities(
[FromQuery] string id,
[FromQuery] string playableMediaTypes,
[FromQuery] string supportedCommands,
[FromQuery] string? id,
[FromQuery] string? playableMediaTypes,
[FromQuery] string? supportedCommands,
[FromQuery] bool supportsMediaControl,
[FromQuery] bool supportsSync,
[FromQuery] bool supportsPersistentIdentifier = true)
@@ -401,7 +401,7 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/Capabilities/Full")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostFullCapabilities(
[FromQuery] string id,
[FromQuery] string? id,
[FromBody, Required] ClientCapabilities capabilities)
{
if (string.IsNullOrWhiteSpace(id))
@@ -424,8 +424,8 @@ namespace Jellyfin.Api.Controllers
[HttpPost("/Sessions/Viewing")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult ReportViewing(
[FromQuery] string sessionId,
[FromQuery] string itemId)
[FromQuery] string? sessionId,
[FromQuery] string? itemId)
{
string session = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;

View File

@@ -75,9 +75,9 @@ namespace Jellyfin.Api.Controllers
[HttpPost("Configuration")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateInitialConfiguration(
[FromForm] string uiCulture,
[FromForm] string metadataCountryCode,
[FromForm] string preferredMetadataLanguage)
[FromForm] string? uiCulture,
[FromForm] string? metadataCountryCode,
[FromForm] string? preferredMetadataLanguage)
{
_config.Configuration.UICulture = uiCulture;
_config.Configuration.MetadataCountryCode = metadataCountryCode;

View File

@@ -0,0 +1,277 @@
using System;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Studios controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class StudiosController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="StudiosController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public StudiosController(
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService)
{
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
}
/// <summary>
/// Gets all studios from a given item, folder, or the entire library.
/// </summary>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="searchTerm">Optional. Search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="userId">User id.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
/// <param name="enableImages">Optional, include image information in output.</param>
/// <param name="enableTotalRecordCount">Total record count.</param>
/// <response code="200">Studios returned.</response>
/// <returns>An <see cref="OkResult"/> containing the studios.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetStudios(
[FromQuery] double? minCommunityRating,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string searchTerm,
[FromQuery] string parentId,
[FromQuery] string fields,
[FromQuery] string excludeItemTypes,
[FromQuery] string includeItemTypes,
[FromQuery] string filters,
[FromQuery] bool? isFavorite,
[FromQuery] string mediaTypes,
[FromQuery] string genres,
[FromQuery] string genreIds,
[FromQuery] string officialRatings,
[FromQuery] string tags,
[FromQuery] string years,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string enableImageTypes,
[FromQuery] string person,
[FromQuery] string personIds,
[FromQuery] string personTypes,
[FromQuery] string studios,
[FromQuery] string studioIds,
[FromQuery] Guid userId,
[FromQuery] string nameStartsWithOrGreater,
[FromQuery] string nameStartsWith,
[FromQuery] string nameLessThan,
[FromQuery] bool? enableImages = true,
[FromQuery] bool enableTotalRecordCount = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (!userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
StartIndex = startIndex,
Limit = limit,
IsFavorite = isFavorite,
NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, ',', true),
OfficialRatings = RequestHelpers.Split(officialRatings, ',', true),
Genres = RequestHelpers.Split(genres, ',', true),
GenreIds = RequestHelpers.GetGuids(genreIds),
StudioIds = RequestHelpers.GetGuids(studioIds),
Person = person,
PersonIds = RequestHelpers.GetGuids(personIds),
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions,
SearchTerm = searchTerm,
EnableTotalRecordCount = enableTotalRecordCount
};
if (!string.IsNullOrWhiteSpace(parentId))
{
if (parentItem is Folder)
{
query.AncestorIds = new[] { new Guid(parentId) };
}
else
{
query.ItemIds = new[] { new Guid(parentId) };
}
}
// Studios
if (!string.IsNullOrEmpty(studios))
{
query.StudioIds = studios.Split('|').Select(i =>
{
try
{
return _libraryManager.GetStudio(i);
}
catch
{
return null;
}
}).Where(i => i != null).Select(i => i!.Id)
.ToArray();
}
foreach (var filter in RequestHelpers.GetFilters(filters))
{
switch (filter)
{
case ItemFilter.Dislikes:
query.IsLiked = false;
break;
case ItemFilter.IsFavorite:
query.IsFavorite = true;
break;
case ItemFilter.IsFavoriteOrLikes:
query.IsFavoriteOrLiked = true;
break;
case ItemFilter.IsFolder:
query.IsFolder = true;
break;
case ItemFilter.IsNotFolder:
query.IsFolder = false;
break;
case ItemFilter.IsPlayed:
query.IsPlayed = true;
break;
case ItemFilter.IsResumable:
query.IsResumable = true;
break;
case ItemFilter.IsUnplayed:
query.IsPlayed = false;
break;
case ItemFilter.Likes:
query.IsLiked = true;
break;
}
}
var result = new QueryResult<(BaseItem, ItemCounts)>();
var dtos = result.Items.Select(i =>
{
var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes))
{
dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount;
dto.SeriesCount = itemCounts.SeriesCount;
dto.EpisodeCount = itemCounts.EpisodeCount;
dto.MovieCount = itemCounts.MovieCount;
dto.TrailerCount = itemCounts.TrailerCount;
dto.AlbumCount = itemCounts.AlbumCount;
dto.SongCount = itemCounts.SongCount;
dto.ArtistCount = itemCounts.ArtistCount;
}
return dto;
});
return new QueryResult<BaseItemDto>
{
Items = dtos.ToArray(),
TotalRecordCount = result.TotalRecordCount
};
}
/// <summary>
/// Gets a studio by name.
/// </summary>
/// <param name="name">Studio name.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <response code="200">Studio returned.</response>
/// <returns>An <see cref="OkResult"/> containing the studio.</returns>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetStudio([FromRoute] string name, [FromQuery] Guid userId)
{
var dtoOptions = new DtoOptions().AddClientFields(Request);
var item = _libraryManager.GetStudio(name);
if (!userId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(userId);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
}

View File

@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute] Guid itemId,
[FromRoute] string language,
[FromRoute] string? language,
[FromQuery] bool? isPerfectMatch)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -132,7 +132,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute] Guid itemId,
[FromRoute] string subtitleId)
[FromRoute] string? subtitleId)
{
var video = (Video)_libraryManager.GetItemById(itemId);
@@ -161,7 +161,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string? id)
{
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
@@ -186,9 +186,9 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] string mediaSourceId,
[FromRoute, Required] string? mediaSourceId,
[FromRoute, Required] int index,
[FromRoute, Required] string format,
[FromRoute, Required] string? format,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps,
[FromQuery] bool addVttTimeMap,
@@ -254,7 +254,7 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult> GetSubtitlePlaylist(
[FromRoute] Guid itemId,
[FromRoute] int index,
[FromRoute] string mediaSourceId,
[FromRoute] string? mediaSourceId,
[FromQuery, Required] int segmentLength)
{
var item = (Video)_libraryManager.GetItemById(itemId);
@@ -324,7 +324,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
private Task<Stream> EncodeSubtitles(
Guid id,
string mediaSourceId,
string? mediaSourceId,
int index,
string format,
long startPositionTicks,

View File

@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
[HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult GetLogFile([FromQuery, Required] string name)
public ActionResult GetLogFile([FromQuery, Required] string? name)
{
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));

View File

@@ -185,14 +185,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="sortOrder">Optional. Sort order: Ascending,Descending.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
[HttpGet("{seriesId}/Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "sortOrder", Justification = "Imported from ServiceStack")]
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
[FromRoute] string seriesId,
[FromRoute] string? seriesId,
[FromQuery] Guid userId,
[FromQuery] string? fields,
[FromQuery] int? season,
@@ -206,8 +204,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] string? sortBy,
[FromQuery] SortOrder? sortOrder)
[FromQuery] string? sortBy)
{
var user = _userManager.GetUserById(userId);
@@ -314,12 +311,12 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
[FromRoute] string seriesId,
[FromRoute] string? seriesId,
[FromQuery] Guid userId,
[FromQuery] string fields,
[FromQuery] string? fields,
[FromQuery] bool? isSpecialSeason,
[FromQuery] bool? isMissing,
[FromQuery] string adjacentTo,
[FromQuery] string? adjacentTo,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,

View File

@@ -68,17 +68,14 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="isHidden">Optional filter by IsHidden=true or false.</param>
/// <param name="isDisabled">Optional filter by IsDisabled=true or false.</param>
/// <param name="isGuest">Optional filter by IsGuest=true or false.</param>
/// <response code="200">Users returned.</response>
/// <returns>An <see cref="IEnumerable{UserDto}"/> containing the users.</returns>
[HttpGet]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "isGuest", Justification = "Imported from ServiceStack")]
public ActionResult<IEnumerable<UserDto>> GetUsers(
[FromQuery] bool? isHidden,
[FromQuery] bool? isDisabled,
[FromQuery] bool? isGuest)
[FromQuery] bool? isDisabled)
{
var users = Get(isHidden, isDisabled, false, false);
return Ok(users);
@@ -167,8 +164,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<AuthenticationResult>> AuthenticateUser(
[FromRoute, Required] Guid userId,
[FromQuery, BindRequired] string pw,
[FromQuery, BindRequired] string password)
[FromQuery, BindRequired] string? pw,
[FromQuery, BindRequired] string? password)
{
var user = _userManager.GetUserById(userId);
@@ -486,7 +483,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task"/> containing a <see cref="ForgotPasswordResult"/>.</returns>
[HttpPost("ForgotPassword")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string enteredUsername)
public async Task<ActionResult<ForgotPasswordResult>> ForgotPassword([FromBody] string? enteredUsername)
{
var isLocal = HttpContext.Connection.RemoteIpAddress.Equals(HttpContext.Connection.LocalIpAddress)
|| _networkManager.IsInLocalNetwork(HttpContext.Connection.RemoteIpAddress.ToString());
@@ -504,7 +501,7 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
[HttpPost("ForgotPassword/Pin")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string pin)
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
{
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
return result;

View File

@@ -0,0 +1,391 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// User library controller.
/// </summary>
[Authorize(Policy = Policies.DefaultAuthorization)]
public class UserLibraryController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
private readonly ILibraryManager _libraryManager;
private readonly IDtoService _dtoService;
private readonly IUserViewManager _userViewManager;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="UserLibraryController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
public UserLibraryController(
IUserManager userManager,
IUserDataManager userDataRepository,
ILibraryManager libraryManager,
IDtoService dtoService,
IUserViewManager userViewManager,
IFileSystem fileSystem)
{
_userManager = userManager;
_userDataRepository = userDataRepository;
_libraryManager = libraryManager;
_dtoService = dtoService;
_userViewManager = userViewManager;
_fileSystem = fileSystem;
}
/// <summary>
/// Gets an item from a user's library.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item returned.</response>
/// <returns>An <see cref="OkResult"/> containing the d item.</returns>
[HttpGet("/Users/{userId}/Items/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<BaseItemDto>> GetItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
await RefreshItemOnDemandIfNeeded(item).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(Request);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
/// <summary>
/// Gets the root folder from a user's library.
/// </summary>
/// <param name="userId">User id.</param>
/// <response code="200">Root folder returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user's root folder.</returns>
[HttpGet("/Users/{userId}/Items/Root")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<BaseItemDto> GetRootFolder([FromRoute] Guid userId)
{
var user = _userManager.GetUserById(userId);
var item = _libraryManager.GetUserRootFolder();
var dtoOptions = new DtoOptions().AddClientFields(Request);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
/// <summary>
/// Gets intros to play before the main media item plays.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Intros returned.</response>
/// <returns>An <see cref="OkResult"/> containing the intros to play.</returns>
[HttpGet("/Users/{userId}/Items/{itemId}/Intros")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetIntros([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
var items = await _libraryManager.GetIntros(item, user).ConfigureAwait(false);
var dtoOptions = new DtoOptions().AddClientFields(Request);
var dtos = items.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user)).ToArray();
return new QueryResult<BaseItemDto>
{
Items = dtos,
TotalRecordCount = dtos.Length
};
}
/// <summary>
/// Marks an item as a favorite.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item marked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("/Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> MarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
return MarkFavorite(userId, itemId, true);
}
/// <summary>
/// Unmarks item as a favorite.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Item unmarked as favorite.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("/Users/{userId}/FavoriteItems/{itemId}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UnmarkFavoriteItem([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
return MarkFavorite(userId, itemId, false);
}
/// <summary>
/// Deletes a user's saved personal rating for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Personal rating removed.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpDelete("/Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> DeleteUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
return UpdateUserItemRatingInternal(userId, itemId, null);
}
/// <summary>
/// Updates a user's rating for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <param name="likes">Whether this <see cref="UpdateUserItemRating" /> is likes.</param>
/// <response code="200">Item rating updated.</response>
/// <returns>An <see cref="OkResult"/> containing the <see cref="UserItemDataDto"/>.</returns>
[HttpPost("/Users/{userId}/Items/{itemId}/Rating")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<UserItemDataDto> UpdateUserItemRating([FromRoute] Guid userId, [FromRoute] Guid itemId, [FromQuery] bool likes)
{
return UpdateUserItemRatingInternal(userId, itemId, likes);
}
/// <summary>
/// Gets local trailers for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">An <see cref="OkResult"/> containing the item's local trailers.</response>
/// <returns>The items local trailers.</returns>
[HttpGet("/Users/{userId}/Items/{itemId}/LocalTrailers")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLocalTrailers([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
var dtoOptions = new DtoOptions().AddClientFields(Request);
var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
.ToArray();
if (item is IHasTrailers hasTrailers)
{
var trailers = hasTrailers.GetTrailers();
var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
dtosExtras.CopyTo(allTrailers, 0);
dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
return allTrailers;
}
return dtosExtras;
}
/// <summary>
/// Gets special features for an item.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="itemId">Item id.</param>
/// <response code="200">Special features returned.</response>
/// <returns>An <see cref="OkResult"/> containing the special features.</returns>
[HttpGet("/Users/{userId}/Items/{itemId}/SpecialFeatures")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetSpecialFeatures([FromRoute] Guid userId, [FromRoute] Guid itemId)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty)
? _libraryManager.GetUserRootFolder()
: _libraryManager.GetItemById(itemId);
var dtoOptions = new DtoOptions().AddClientFields(Request);
return Ok(item
.GetExtras(BaseItem.DisplayExtraTypes)
.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
}
/// <summary>
/// Gets latest media.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, SortName, Studios, Taglines.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param>
/// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. include user data.</param>
/// <param name="limit">Return item limit.</param>
/// <param name="groupItems">Whether or not to group items into a parent container.</param>
/// <response code="200">Latest media returned.</response>
/// <returns>An <see cref="OkResult"/> containing the latest media.</returns>
[HttpGet("/Users/{userId}/Items/Latest")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
[FromRoute] Guid userId,
[FromQuery] Guid parentId,
[FromQuery] string? fields,
[FromQuery] string? includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] bool? enableUserData,
[FromQuery] int limit = 20,
[FromQuery] bool groupItems = true)
{
var user = _userManager.GetUserById(userId);
if (!isPlayed.HasValue)
{
if (user.HidePlayedInLatest)
{
isPlayed = false;
}
}
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
var list = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
GroupItems = groupItems,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId,
UserId = userId,
}, dtoOptions);
var dtos = list.Select(i =>
{
var item = i.Item2[0];
var childCount = 0;
if (i.Item1 != null && (i.Item2.Count > 1 || i.Item1 is MusicAlbum))
{
item = i.Item1;
childCount = i.Item2.Count;
}
var dto = _dtoService.GetBaseItemDto(item, dtoOptions, user);
dto.ChildCount = childCount;
return dto;
});
return Ok(dtos);
}
private async Task RefreshItemOnDemandIfNeeded(BaseItem item)
{
if (item is Person)
{
var hasMetdata = !string.IsNullOrWhiteSpace(item.Overview) && item.HasImage(ImageType.Primary);
var performFullRefresh = !hasMetdata && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= 3;
if (!hasMetdata)
{
var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ForceSave = performFullRefresh
};
await item.RefreshMetadata(options, CancellationToken.None).ConfigureAwait(false);
}
}
}
/// <summary>
/// Marks the favorite.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="isFavorite">if set to <c>true</c> [is favorite].</param>
private UserItemDataDto MarkFavorite(Guid userId, Guid itemId, bool isFavorite)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
// Set favorite status
data.IsFavorite = isFavorite;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
return _userDataRepository.GetUserDataDto(item, user);
}
/// <summary>
/// Updates the user item rating.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="likes">if set to <c>true</c> [likes].</param>
private UserItemDataDto UpdateUserItemRatingInternal(Guid userId, Guid itemId, bool? likes)
{
var user = _userManager.GetUserById(userId);
var item = itemId.Equals(Guid.Empty) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId);
// Get the user data for this item
var data = _userDataRepository.GetUserData(user, item);
data.Likes = likes;
_userDataRepository.SaveUserData(user, item, data, UserDataSaveReason.UpdateUserRating, CancellationToken.None);
return _userDataRepository.GetUserDataDto(item, user);
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// User views controller.
/// </summary>
public class UserViewsController : BaseJellyfinApiController
{
private readonly IUserManager _userManager;
private readonly IUserViewManager _userViewManager;
private readonly IDtoService _dtoService;
private readonly IAuthorizationContext _authContext;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="UserViewsController"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="userViewManager">Instance of the <see cref="IUserViewManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="authContext">Instance of the <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public UserViewsController(
IUserManager userManager,
IUserViewManager userViewManager,
IDtoService dtoService,
IAuthorizationContext authContext,
ILibraryManager libraryManager)
{
_userManager = userManager;
_userViewManager = userViewManager;
_dtoService = dtoService;
_authContext = authContext;
_libraryManager = libraryManager;
}
/// <summary>
/// Get user views.
/// </summary>
/// <param name="userId">User id.</param>
/// <param name="includeExternalContent">Whether or not to include external views such as channels or live tv.</param>
/// <param name="includeHidden">Whether or not to include hidden content.</param>
/// <param name="presetViews">Preset views.</param>
/// <response code="200">User views returned.</response>
/// <returns>An <see cref="OkResult"/> containing the user views.</returns>
[HttpGet("/Users/{userId}/Views")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
[FromRoute] Guid userId,
[FromQuery] bool? includeExternalContent,
[FromQuery] bool includeHidden,
[FromQuery] string? presetViews)
{
var query = new UserViewQuery
{
UserId = userId,
IncludeHidden = includeHidden
};
if (includeExternalContent.HasValue)
{
query.IncludeExternalContent = includeExternalContent.Value;
}
if (!string.IsNullOrWhiteSpace(presetViews))
{
query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
}
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
if (app.IndexOf("emby rt", StringComparison.OrdinalIgnoreCase) != -1)
{
query.PresetViews = new[] { CollectionType.Movies, CollectionType.TvShows };
}
var folders = _userViewManager.GetUserViews(query);
var dtoOptions = new DtoOptions().AddClientFields(Request);
var fields = dtoOptions.Fields.ToList();
fields.Add(ItemFields.PrimaryImageAspectRatio);
fields.Add(ItemFields.DisplayPreferencesId);
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray();
var user = _userManager.GetUserById(userId);
var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
.ToArray();
return new QueryResult<BaseItemDto>
{
Items = dtos,
TotalRecordCount = dtos.Length
};
}
/// <summary>
/// Get user view grouping options.
/// </summary>
/// <param name="userId">User id.</param>
/// <response code="200">User view grouping options returned.</response>
/// <response code="404">User not found.</response>
/// <returns>
/// An <see cref="OkResult"/> containing the user view grouping options
/// or a <see cref="NotFoundResult"/> if user not found.
/// </returns>
[HttpGet("/Users/{userId}/GroupingOptions")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<SpecialViewOptionDto>> GetGroupingOptions([FromRoute] Guid userId)
{
var user = _userManager.GetUserById(userId);
if (user == null)
{
return NotFound();
}
return Ok(_libraryManager.GetUserRootFolder()
.GetChildren(user, true)
.OfType<Folder>()
.Where(UserView.IsEligibleForGrouping)
.Select(i => new SpecialViewOptionDto
{
Name = i.Name,
Id = i.Id.ToString("N", CultureInfo.InvariantCulture)
})
.OrderBy(i => i.Name));
}
}
}

View File

@@ -50,7 +50,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<FileStreamResult>> GetAttachment(
[FromRoute] Guid videoId,
[FromRoute] string mediaSourceId,
[FromRoute] string? mediaSourceId,
[FromRoute] int index)
{
try

View File

@@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public ActionResult MergeVersions([FromQuery] string itemIds)
public ActionResult MergeVersions([FromQuery] string? itemIds)
{
var items = RequestHelpers.Split(itemIds, ',', true)
.Select(i => _libraryManager.GetItemById(i))

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Years controller.
/// </summary>
public class YearsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDtoService _dtoService;
/// <summary>
/// Initializes a new instance of the <see cref="YearsController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
public YearsController(
ILibraryManager libraryManager,
IUserManager userManager,
IDtoService dtoService)
{
_libraryManager = libraryManager;
_userManager = userManager;
_dtoService = dtoService;
}
/// <summary>
/// Get years.
/// </summary>
/// <param name="startIndex">Skips over a given number of items within the results. Use for paging.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be excluded based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be included based on item type. This allows multiple, comma delimited.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="userId">User Id.</param>
/// <param name="recursive">Search recursively.</param>
/// <param name="enableImages">Optional. Include image information in output.</param>
/// <response code="200">Year query returned.</response>
/// <returns> A <see cref="QueryResult{BaseItemDto}"/> containing the year result.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetYears(
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string? sortOrder,
[FromQuery] string? parentId,
[FromQuery] string? fields,
[FromQuery] string? excludeItemTypes,
[FromQuery] string? includeItemTypes,
[FromQuery] string? mediaTypes,
[FromQuery] string? sortBy,
[FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit,
[FromQuery] string? enableImageTypes,
[FromQuery] Guid userId,
[FromQuery] bool recursive = true,
[FromQuery] bool? enableImages = true)
{
var dtoOptions = new DtoOptions()
.AddItemFields(fields)
.AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
User? user = null;
BaseItem parentItem;
if (!userId.Equals(Guid.Empty))
{
user = _userManager.GetUserById(userId);
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
}
else
{
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
}
IList<BaseItem> items;
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
var query = new InternalItemsQuery(user)
{
ExcludeItemTypes = excludeItemTypesArr,
IncludeItemTypes = includeItemTypesArr,
MediaTypes = mediaTypesArr,
DtoOptions = dtoOptions
};
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
if (parentItem.IsFolder)
{
var folder = (Folder)parentItem;
if (!userId.Equals(Guid.Empty))
{
items = recursive ? folder.GetRecursiveChildren(user, query).ToList() : folder.GetChildren(user, true).Where(Filter).ToList();
}
else
{
items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
}
}
else
{
items = new[] { parentItem }.Where(Filter).ToList();
}
var extractedItems = GetAllItems(items);
var filteredItems = _libraryManager.Sort(extractedItems, user, RequestHelpers.GetOrderBy(sortBy, sortOrder));
var ibnItemsArray = filteredItems.ToList();
IEnumerable<BaseItem> ibnItems = ibnItemsArray;
var result = new QueryResult<BaseItemDto> { TotalRecordCount = ibnItemsArray.Count };
if (startIndex.HasValue || limit.HasValue)
{
if (startIndex.HasValue)
{
ibnItems = ibnItems.Skip(startIndex.Value);
}
if (limit.HasValue)
{
ibnItems = ibnItems.Take(limit.Value);
}
}
var tuples = ibnItems.Select(i => new Tuple<BaseItem, List<BaseItem>>(i, new List<BaseItem>()));
var dtos = tuples.Select(i => _dtoService.GetItemByNameDto(i.Item1, dtoOptions, i.Item2, user));
result.Items = dtos.Where(i => i != null).ToArray();
return result;
}
/// <summary>
/// Gets a year.
/// </summary>
/// <param name="year">The year.</param>
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
/// <response code="200">Year returned.</response>
/// <response code="404">Year not found.</response>
/// <returns>
/// An <see cref="OkResult"/> containing the year,
/// or a <see cref="NotFoundResult"/> if year not found.
/// </returns>
[HttpGet("{year}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<BaseItemDto> GetYear([FromRoute] int year, [FromQuery] Guid userId)
{
var item = _libraryManager.GetYear(year);
if (item == null)
{
return NotFound();
}
var dtoOptions = new DtoOptions()
.AddClientFields(Request);
if (!userId.Equals(Guid.Empty))
{
var user = _userManager.GetUserById(userId);
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
}
return _dtoService.GetBaseItemDto(item, dtoOptions);
}
private bool FilterItem(BaseItem f, IReadOnlyCollection<string> excludeItemTypes, IReadOnlyCollection<string> includeItemTypes, IReadOnlyCollection<string> mediaTypes)
{
// Exclude item types
if (excludeItemTypes.Count > 0 && excludeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
// Include item types
if (includeItemTypes.Count > 0 && !includeItemTypes.Contains(f.GetType().Name, StringComparer.OrdinalIgnoreCase))
{
return false;
}
// Include MediaTypes
if (mediaTypes.Count > 0 && !mediaTypes.Contains(f.MediaType ?? string.Empty, StringComparer.OrdinalIgnoreCase))
{
return false;
}
return true;
}
private IEnumerable<BaseItem> GetAllItems(IEnumerable<BaseItem> items)
{
return items
.Select(i => i.ProductionYear ?? 0)
.Where(i => i > 0)
.Distinct()
.Select(year => _libraryManager.GetYear(year));
}
}
}