mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-10 00:02:56 +01:00
Merge branch 'master' into syncplay-enhanced
This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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>
|
||||
[Route("")]
|
||||
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, Required] 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, Required] 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -51,7 +53,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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.</param>
|
||||
@@ -86,25 +88,25 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@@ -112,8 +114,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -130,30 +131,26 @@ namespace Jellyfin.Api.Controllers
|
||||
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,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
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),
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
@@ -173,9 +170,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -229,7 +226,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
@@ -260,7 +257,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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.</param>
|
||||
@@ -295,25 +292,25 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@@ -321,8 +318,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers
|
||||
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,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
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),
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
@@ -382,9 +374,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
@@ -31,6 +31,171 @@ namespace Jellyfin.Api.Controllers
|
||||
_audioHelper = audioHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an audio stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The audio container.</param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Audio stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||
{
|
||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||
{
|
||||
Id = itemId,
|
||||
Container = container,
|
||||
Static = @static ?? true,
|
||||
Params = @params,
|
||||
Tag = tag,
|
||||
DeviceProfileId = deviceProfileId,
|
||||
PlaySessionId = playSessionId,
|
||||
SegmentContainer = segmentContainer,
|
||||
SegmentLength = segmentLength,
|
||||
MinSegments = minSegments,
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
Level = level,
|
||||
Framerate = framerate,
|
||||
MaxFramerate = maxFramerate,
|
||||
CopyTimestamps = copyTimestamps ?? true,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
Width = width,
|
||||
Height = height,
|
||||
VideoBitRate = videoBitRate,
|
||||
SubtitleStreamIndex = subtitleStreamIndex,
|
||||
SubtitleMethod = subtitleMethod,
|
||||
MaxRefFrames = maxRefFrames,
|
||||
MaxVideoBitDepth = maxVideoBitDepth,
|
||||
RequireAvc = requireAvc ?? true,
|
||||
DeInterlace = deInterlace ?? true,
|
||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||
CpuCoreLimit = cpuCoreLimit,
|
||||
LiveStreamId = liveStreamId,
|
||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
||||
VideoCodec = videoCodec,
|
||||
SubtitleCodec = subtitleCodec,
|
||||
TranscodeReasons = transcodingReasons,
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
VideoStreamIndex = videoStreamIndex,
|
||||
Context = context ?? EncodingContext.Static,
|
||||
StreamOptions = streamOptions
|
||||
};
|
||||
|
||||
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an audio stream.
|
||||
/// </summary>
|
||||
@@ -85,15 +250,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Audio stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
|
||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||
[HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
|
||||
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Branding;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -107,7 +108,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply.</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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <response code="200">Channel items returned.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="Task"/> representing the request to get the channel items.
|
||||
@@ -121,9 +122,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] string? fields)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
@@ -136,8 +137,7 @@ namespace Jellyfin.Api.Controllers
|
||||
ChannelIds = new[] { channelId },
|
||||
ParentId = folderId ?? Guid.Empty,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
DtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
DtoOptions = new DtoOptions { Fields = fields }
|
||||
};
|
||||
|
||||
foreach (var filter in filters)
|
||||
@@ -184,7 +184,7 @@ 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="filters">Optional. Specify additional filters to apply.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
||||
/// <response code="200">Latest channel items returned.</response>
|
||||
/// <returns>
|
||||
@@ -196,9 +196,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? channelIds)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
@@ -208,13 +208,8 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
ChannelIds = (channelIds ?? string.Empty)
|
||||
.Split(',')
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new Guid(i))
|
||||
.ToArray(),
|
||||
DtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
ChannelIds = channelIds,
|
||||
DtoOptions = new DtoOptions { Fields = fields }
|
||||
};
|
||||
|
||||
foreach (var filter in filters)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] bool isLocked = false)
|
||||
{
|
||||
@@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsLocked = isLocked,
|
||||
Name = name,
|
||||
ParentId = parentId,
|
||||
ItemIdList = RequestHelpers.Split(ids, ',', true),
|
||||
ItemIdList = ids,
|
||||
UserIds = new[] { userId }
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
@@ -83,14 +84,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Adds items to a collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection id.</param>
|
||||
/// <param name="itemIds">Item ids, comma delimited.</param>
|
||||
/// <param name="ids">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 async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
|
||||
public async Task<ActionResult> AddToCollection(
|
||||
[FromRoute, Required] Guid collectionId,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(true);
|
||||
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -98,14 +101,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Removes items from a collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection id.</param>
|
||||
/// <param name="itemIds">Item ids, comma delimited.</param>
|
||||
/// <param name="ids">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 async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string itemIds)
|
||||
public async Task<ActionResult> RemoveFromCollection(
|
||||
[FromRoute, Required] Guid collectionId,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
{
|
||||
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(itemIds)).ConfigureAwait(false);
|
||||
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Devices Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
public class DevicesController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
@@ -46,7 +46,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Devices retrieved.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<DeviceInfo>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||
{
|
||||
@@ -62,7 +61,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Info")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceInfo> GetDeviceInfo([FromQuery, Required] string id)
|
||||
@@ -84,7 +82,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpGet("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<DeviceOptions> GetDeviceOptions([FromQuery, Required] string id)
|
||||
@@ -107,7 +104,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Device not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||
[HttpPost("Options")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateDeviceOptions(
|
||||
|
||||
@@ -81,6 +81,9 @@ namespace Jellyfin.Api.Controllers
|
||||
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||
|
||||
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
|
||||
_displayPreferencesManager.SaveChanges();
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
||||
@@ -94,6 +95,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Gets Dlna media receiver registrar xml.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||
[HttpGet("{serverId}/ConnectionManager")]
|
||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
||||
@@ -111,8 +113,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Process a content directory control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||
@@ -122,8 +128,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Process a connection manager control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||
@@ -133,8 +143,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Process a media receiver registrar control request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Control response.</returns>
|
||||
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||
{
|
||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||
@@ -144,11 +158,15 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||
@@ -158,11 +176,15 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_contentDirectory);
|
||||
@@ -172,11 +194,15 @@ namespace Jellyfin.Api.Controllers
|
||||
/// Processes an event subscription request.
|
||||
/// </summary>
|
||||
/// <param name="serverId">Server UUID.</param>
|
||||
/// <response code="200">Request processed.</response>
|
||||
/// <returns>Event subscription response.</returns>
|
||||
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Text.Xml)]
|
||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||
{
|
||||
return ProcessEventRequest(_connectionManager);
|
||||
@@ -226,7 +252,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private string GetAbsoluteUri()
|
||||
{
|
||||
return $"{Request.Scheme}://{Request.Host}{Request.Path}";
|
||||
return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
|
||||
}
|
||||
|
||||
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -14,6 +15,7 @@ using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
@@ -40,6 +42,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class DynamicHlsController : BaseJellyfinApiController
|
||||
{
|
||||
private const string DefaultEncoderPreset = "veryfast";
|
||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
@@ -55,8 +60,7 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly ILogger<DynamicHlsController> _logger;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||
|
||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
|
||||
private readonly EncodingOptions _encodingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
|
||||
@@ -91,6 +95,8 @@ namespace Jellyfin.Api.Controllers
|
||||
ILogger<DynamicHlsController> logger,
|
||||
DynamicHlsHelper dynamicHlsHelper)
|
||||
{
|
||||
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
|
||||
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
@@ -105,8 +111,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_logger = logger;
|
||||
_dynamicHlsHelper = dynamicHlsHelper;
|
||||
|
||||
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -119,7 +124,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -148,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -271,7 +276,7 @@ namespace Jellyfin.Api.Controllers
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -284,7 +289,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -295,6 +300,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
@@ -313,7 +319,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -351,6 +357,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
@@ -403,7 +410,7 @@ namespace Jellyfin.Api.Controllers
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
@@ -436,7 +443,7 @@ namespace Jellyfin.Api.Controllers
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -449,7 +456,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -478,7 +485,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -612,7 +619,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -623,6 +630,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
@@ -641,7 +649,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -677,6 +685,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
@@ -729,7 +738,7 @@ namespace Jellyfin.Api.Controllers
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
@@ -807,7 +816,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -829,7 +838,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute] string container,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
@@ -948,7 +957,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -959,6 +968,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
@@ -977,7 +987,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -999,7 +1009,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute] string container,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
@@ -1017,6 +1027,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
@@ -1069,7 +1080,7 @@ namespace Jellyfin.Api.Controllers
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||
AudioSampleRate = audioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = audioBitRate,
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = audioChannels,
|
||||
Profile = profile,
|
||||
@@ -1122,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -1130,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var segmentLengths = GetSegmentLengths(state);
|
||||
|
||||
var segmentContainer = state.Request.SegmentContainer ?? "ts";
|
||||
|
||||
// http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
|
||||
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
|
||||
var hlsVersion = isHlsInFmp4 ? "7" : "3";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("#EXTM3U")
|
||||
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||
.AppendLine("#EXT-X-VERSION:3")
|
||||
.Append("#EXT-X-VERSION:")
|
||||
.Append(hlsVersion)
|
||||
.AppendLine()
|
||||
.Append("#EXT-X-TARGETDURATION:")
|
||||
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
|
||||
.AppendLine()
|
||||
@@ -1144,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
|
||||
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
|
||||
var queryString = Request.QueryString;
|
||||
|
||||
if (isHlsInFmp4)
|
||||
{
|
||||
builder.Append("#EXT-X-MAP:URI=\"")
|
||||
.Append("hls1/")
|
||||
.Append(name)
|
||||
.Append("/-1")
|
||||
.Append(segmentExtension)
|
||||
.Append(queryString)
|
||||
.Append('"')
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var length in segmentLengths)
|
||||
{
|
||||
builder.Append("#EXTINF:")
|
||||
@@ -1187,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@@ -1201,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (System.IO.File.Exists(segmentPath))
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@@ -1215,7 +1246,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
if (System.IO.File.Exists(segmentPath))
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
transcodingLock.Release();
|
||||
released = true;
|
||||
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
|
||||
@@ -1226,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
|
||||
if (currentTranscodingIndex == null)
|
||||
if (segmentId == -1)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
|
||||
startTranscoding = true;
|
||||
segmentId = 0;
|
||||
}
|
||||
else if (currentTranscodingIndex == null)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
|
||||
startTranscoding = true;
|
||||
@@ -1258,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
|
||||
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
|
||||
|
||||
state.WaitForPath = segmentPath;
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
job = await _transcodingJobHelper.StartFfMpeg(
|
||||
state,
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
|
||||
GetCommandLineArguments(playlistPath, state, true, segmentId),
|
||||
Request,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
@@ -1277,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
if (job?.TranscodingThrottler != null)
|
||||
{
|
||||
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
|
||||
@@ -1294,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
_logger.LogDebug("returning {0} [general case]", segmentPath);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -1318,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
|
||||
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
|
||||
|
||||
if (state.BaseRequest.BreakOnNonKeyFrames)
|
||||
{
|
||||
@@ -1334,34 +1369,57 @@ namespace Jellyfin.Api.Controllers
|
||||
state.BaseRequest.BreakOnNonKeyFrames = false;
|
||||
}
|
||||
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
|
||||
|
||||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
|
||||
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
|
||||
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
||||
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
|
||||
var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
var outputTsArg = outputPrefix + "%d" + outputExtension;
|
||||
|
||||
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
|
||||
var segmentFormat = outputExtension.TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
|
||||
? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid HLS segment container: " + segmentFormat);
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
|
||||
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
: "128";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
|
||||
inputModifier,
|
||||
_encodingHelper.GetInputArgument(state, encodingOptions),
|
||||
_encodingHelper.GetInputArgument(state, _encodingOptions),
|
||||
threads,
|
||||
mapArgs,
|
||||
GetVideoArguments(state, encodingOptions, startNumber),
|
||||
GetAudioArguments(state, encodingOptions),
|
||||
GetVideoArguments(state, startNumber),
|
||||
GetAudioArguments(state),
|
||||
maxMuxingQueueSize,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||
segmentFormat,
|
||||
@@ -1370,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
|
||||
outputPath).Trim();
|
||||
}
|
||||
|
||||
private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
|
||||
/// <summary>
|
||||
/// Gets the audio arguments for transcoding.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <returns>The command line arguments for audio transcoding.</returns>
|
||||
private string GetAudioArguments(StreamState state)
|
||||
{
|
||||
if (state.AudioStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return "-acodec copy";
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = new List<string>();
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams.Add("-acodec " + audioCodec);
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioChannels.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams.Add("-vn");
|
||||
return string.Join(' ', audioTranscodeParams);
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
||||
{
|
||||
return "-codec:a:0 copy -copypriorss:a:0 0";
|
||||
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
|
||||
}
|
||||
|
||||
return "-codec:a:0 copy";
|
||||
return "-codec:a:0 copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
@@ -1437,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
|
||||
/// <summary>
|
||||
/// Gets the video arguments for transcoding.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <param name="startNumber">The first number in the hls sequence.</param>
|
||||
/// <returns>The command line arguments for video transcoding.</returns>
|
||||
private string GetVideoArguments(StreamState state, int startNumber)
|
||||
{
|
||||
if (state.VideoStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
// {
|
||||
// args += " -mpegts_m2ts_mode 1";
|
||||
// }
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
// See if we can save come cpu cycles by avoiding encoding.
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
{
|
||||
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
if (!string.IsNullOrEmpty(bitStreamArgs))
|
||||
{
|
||||
args += " " + bitStreamArgs;
|
||||
}
|
||||
}
|
||||
|
||||
args += " -start_at_zero";
|
||||
|
||||
// args += " -flags -global_header";
|
||||
}
|
||||
else
|
||||
{
|
||||
var gopArg = string.Empty;
|
||||
var keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
|
||||
startNumber * state.SegmentLength,
|
||||
state.SegmentLength);
|
||||
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
|
||||
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
// Set the key frame params for video encoding to match the hls segment time.
|
||||
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
|
||||
|
||||
if (framerate.HasValue)
|
||||
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
|
||||
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This is to make sure keyframe interval is limited to our segment,
|
||||
// as forcing keyframes is not enough.
|
||||
// Example: we encoded half of desired length, then codec detected
|
||||
// scene cut and inserted a keyframe; next forced keyframe would
|
||||
// be created outside of segment, which breaks seeking
|
||||
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
|
||||
gopArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -g {0} -keyint_min {0} -sc_threshold 0",
|
||||
Math.Ceiling(state.SegmentLength * framerate.Value));
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
|
||||
|
||||
// Unable to force key frames using these hw encoders, set key frames by GOP
|
||||
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " " + gopArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
args += " " + keyFrameArg + gopArg;
|
||||
args += " -bf 0";
|
||||
}
|
||||
|
||||
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
// This is for graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
|
||||
// Graphical subs overlay and resolution params.
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// Add resolution params, if specified
|
||||
else
|
||||
{
|
||||
args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
|
||||
// Resolution params.
|
||||
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// -start_at_zero is necessary to use with -ss when seeking,
|
||||
@@ -1559,8 +1625,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private string GetSegmentPath(StreamState state, string playlist, int index)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(playlist);
|
||||
|
||||
var folder = Path.GetDirectoryName(playlist) ?? throw new ArgumentException($"Provided path ({playlist}) is not valid.", nameof(playlist));
|
||||
var filename = Path.GetFileNameWithoutExtension(playlist);
|
||||
|
||||
return Path.Combine(folder, filename + index.ToString(CultureInfo.InvariantCulture) + GetSegmentFileExtension(state.Request.SegmentContainer));
|
||||
@@ -1685,7 +1750,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
|
||||
{
|
||||
var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
|
||||
var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
|
||||
|
||||
if (job == null || job.HasExited)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.EnvironmentDtos;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -16,7 +17,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Environment Controller.
|
||||
/// </summary>
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||
public class EnvironmentController : BaseJellyfinApiController
|
||||
{
|
||||
private const char UncSeparator = '\\';
|
||||
@@ -103,6 +104,11 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (validatePathDto.ValidateWritable)
|
||||
{
|
||||
if (validatePathDto.Path == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(validatePathDto.Path));
|
||||
}
|
||||
|
||||
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? mediaTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
|
||||
{
|
||||
var parentItem = string.IsNullOrEmpty(parentId)
|
||||
? null
|
||||
@@ -61,10 +62,11 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
@@ -78,8 +80,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
User = user,
|
||||
MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
MediaTypes = mediaTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
Recursive = true,
|
||||
EnableTotalRecordCount = false,
|
||||
DtoOptions = new DtoOptions
|
||||
@@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryFilters> GetQueryFilters(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isAiring,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSports,
|
||||
@@ -156,10 +158,11 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
@@ -167,8 +170,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var filters = new QueryFilters();
|
||||
var genreQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes =
|
||||
(includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = Array.Empty<ItemFields>(),
|
||||
@@ -192,10 +194,11 @@ namespace Jellyfin.Api.Controllers
|
||||
genreQuery.Parent = parentItem;
|
||||
}
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||
{
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets all genres 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="fields">Optional. Specify additional fields of information to return in the output.</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.</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>
|
||||
@@ -82,30 +69,16 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
|
||||
[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] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@@ -113,45 +86,24 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
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
|
||||
@@ -169,87 +121,20 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
QueryResult<(BaseItem, ItemCounts)> result;
|
||||
if (parentItem is ICollectionFolder parentCollectionFolder
|
||||
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|
||||
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
|
||||
{
|
||||
query.StudioIds = studios.Split('|')
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null)
|
||||
.Select(i => i!.Id)
|
||||
.ToArray();
|
||||
result = _libraryManager.GetMusicGenres(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _libraryManager.GetGenres(query);
|
||||
}
|
||||
|
||||
foreach (var filter in 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
|
||||
};
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -291,7 +176,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
}
|
||||
|
||||
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
||||
private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
@@ -8,6 +8,7 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
@@ -111,11 +112,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="segmentId">The segment id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <response code="200">Hls video segment returned.</response>
|
||||
/// <response code="404">Hls segment not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
|
||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||
// [Authenticated]
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsVideoSegmentLegacy(
|
||||
@@ -131,12 +134,25 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var normalizedPlaylistId = playlistId;
|
||||
|
||||
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
|
||||
.FirstOrDefault(i =>
|
||||
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
|
||||
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1);
|
||||
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
||||
// Add . to start of segment container for future use.
|
||||
segmentContainer = segmentContainer.Insert(0, ".");
|
||||
string? playlistPath = null;
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
var pathExtension = Path.GetExtension(path);
|
||||
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
playlistPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GetFileResult(file, playlistPath);
|
||||
return playlistPath == null
|
||||
? NotFound("Hls segment not found.")
|
||||
: GetFileResult(file, playlistPath);
|
||||
}
|
||||
|
||||
private ActionResult GetFileResult(string path, string playlistPath)
|
||||
|
||||
@@ -161,7 +161,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 GetImageFile(string basePath, string? theme, string? name)
|
||||
private ActionResult GetImageFile(string basePath, string theme, string? name)
|
||||
{
|
||||
var themeFolder = Path.Combine(basePath, theme);
|
||||
if (Directory.Exists(themeFolder))
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@@ -85,7 +86,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="403">User does not have permission to delete the image.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||
[HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
@@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult> PostUserImage(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? index = null)
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
@@ -109,7 +109,53 @@ namespace Jellyfin.Api.Controllers
|
||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||
if (user.ProfileImage != null)
|
||||
{
|
||||
_userManager.ClearProfileImage(user);
|
||||
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user image.
|
||||
/// </summary>
|
||||
/// <param name="userId">User Id.</param>
|
||||
/// <param name="imageType">(Unused) Image type.</param>
|
||||
/// <param name="index">(Unused) Image index.</param>
|
||||
/// <response code="204">Image updated.</response>
|
||||
/// <response code="403">User does not have permission to delete the image.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> PostUserImageByIndex(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int index)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
return Forbid("User is not allowed to update the image.");
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
await using var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
var userDataPath = Path.Combine(_serverConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
|
||||
if (user.ProfileImage != null)
|
||||
{
|
||||
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
|
||||
@@ -131,17 +177,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Image deleted.</response>
|
||||
/// <response code="403">User does not have permission to delete the image.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Users/{userId}/Images/{itemType}")]
|
||||
[HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
|
||||
[HttpDelete("Users/{userId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult DeleteUserImage(
|
||||
public async Task<ActionResult> DeleteUserImage(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? index = null)
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
@@ -158,7 +203,46 @@ namespace Jellyfin.Api.Controllers
|
||||
_logger.LogError(e, "Error deleting user profile image:");
|
||||
}
|
||||
|
||||
_userManager.ClearProfileImage(user);
|
||||
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete the user's image.
|
||||
/// </summary>
|
||||
/// <param name="userId">User Id.</param>
|
||||
/// <param name="imageType">(Unused) Image type.</param>
|
||||
/// <param name="index">(Unused) Image index.</param>
|
||||
/// <response code="204">Image deleted.</response>
|
||||
/// <response code="403">User does not have permission to delete the image.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Users/{userId}/Images/{imageType}/{index}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<ActionResult> DeleteUserImageByIndex(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int index)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
return Forbid("User is not allowed to delete the image.");
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
try
|
||||
{
|
||||
System.IO.File.Delete(user.ProfileImage.Path);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
_logger.LogError(e, "Error deleting user profile image:");
|
||||
}
|
||||
|
||||
await _userManager.ClearProfileImageAsync(user).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -172,14 +256,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteItemImage(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
@@ -191,6 +274,65 @@ namespace Jellyfin.Api.Controllers
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an item's image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">The image index.</param>
|
||||
/// <response code="204">Image deleted.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set item image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <response code="204">Image saved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> SetItemImage(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set item image.
|
||||
/// </summary>
|
||||
@@ -200,16 +342,15 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="204">Image saved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> SetItemImage(
|
||||
public async Task<ActionResult> SetItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromRoute] int imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
@@ -349,8 +490,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </returns>
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -371,7 +510,86 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
itemId,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item's image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int imageIndex,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
@@ -507,8 +725,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
|
||||
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -586,8 +804,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
|
||||
[HttpGet("Genres/{name}/Images/{imageType}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -608,7 +826,86 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get genre image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Genre name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetGenreImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
if (item == null)
|
||||
@@ -665,8 +962,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -687,7 +984,86 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get music genre image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Music genre name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetMusicGenreImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
if (item == null)
|
||||
@@ -744,8 +1120,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
|
||||
[HttpGet("Persons/{name}/Images/{imageType}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -766,7 +1142,86 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get person image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Person name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetPersonImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(name);
|
||||
if (item == null)
|
||||
@@ -823,16 +1278,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
|
||||
[HttpGet("Studios/{name}/Images/{imageType}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetStudioImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] string tag,
|
||||
[FromRoute, Required] ImageFormat format,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
@@ -845,7 +1300,86 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get studio image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Studio name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetStudioImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
if (item == null)
|
||||
@@ -902,8 +1436,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
|
||||
[HttpGet("Users/{userId}/Images/{imageType}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
@@ -924,7 +1458,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
@@ -973,6 +1507,103 @@ namespace Jellyfin.Api.Controllers
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user profile image.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetUserImageByIndex(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user?.ProfileImage == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var info = new ItemImageInfo
|
||||
{
|
||||
Path = user.ProfileImage.Path,
|
||||
Type = ImageType.Profile,
|
||||
DateModified = user.ProfileImage.LastModified
|
||||
};
|
||||
|
||||
if (width.HasValue)
|
||||
{
|
||||
info.Width = width.Value;
|
||||
}
|
||||
|
||||
if (height.HasValue)
|
||||
{
|
||||
info.Height = height.Value;
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
user.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
null,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
|
||||
info)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
@@ -1268,7 +1899,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Response.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
Response.ContentType = imageContentType;
|
||||
Response.ContentType = imageContentType ?? MediaTypeNames.Text.Plain;
|
||||
Response.Headers.Add(HeaderNames.Age, Convert.ToInt64((DateTime.UtcNow - dateImageModified).TotalSeconds).ToString(CultureInfo.InvariantCulture));
|
||||
Response.Headers.Add(HeaderNames.Vary, HeaderNames.Accept);
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
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.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -54,7 +56,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -67,18 +69,17 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
@@ -91,7 +92,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -104,18 +105,17 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var album = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
|
||||
@@ -128,7 +128,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -141,18 +141,17 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
|
||||
@@ -165,7 +164,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -178,17 +177,16 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] string name,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, dtoOptions);
|
||||
@@ -201,31 +199,30 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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")]
|
||||
[HttpGet("Artists/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
@@ -238,31 +235,30 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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")]
|
||||
[HttpGet("MusicGenres/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
@@ -275,7 +271,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -288,18 +284,17 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||
@@ -315,9 +310,9 @@ namespace Jellyfin.Api.Controllers
|
||||
TotalRecordCount = list.Count
|
||||
};
|
||||
|
||||
if (limit.HasValue)
|
||||
if (limit.HasValue && limit < list.Count)
|
||||
{
|
||||
list = list.Take(limit.Value).ToList();
|
||||
list = list.GetRange(0, limit.Value);
|
||||
}
|
||||
|
||||
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
@@ -334,10 +334,16 @@ namespace Jellyfin.Api.Controllers
|
||||
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
|
||||
if (result.Content.Headers.ContentType?.MediaType == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
|
||||
}
|
||||
|
||||
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
using (var stream = result.Content)
|
||||
{
|
||||
await using var fileStream = new FileStream(
|
||||
@@ -351,7 +357,9 @@ namespace Jellyfin.Api.Controllers
|
||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||
|
||||
Directory.CreateDirectory(pointerCacheDirectory);
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -59,7 +60,445 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
/// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
|
||||
/// <param name="userId">The user id supplied as query parameter.</param>
|
||||
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
|
||||
/// <param name="hasThemeVideo">Optional filter by items with theme videos.</param>
|
||||
/// <param name="hasSubtitles">Optional filter by items with subtitles.</param>
|
||||
/// <param name="hasSpecialFeature">Optional filter by items with special features.</param>
|
||||
/// <param name="hasTrailer">Optional filter by items with trailers.</param>
|
||||
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
|
||||
/// <param name="parentIndexNumber">Optional filter by parent index number.</param>
|
||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
/// <param name="minCriticRating">Optional filter by minimum critic rating.</param>
|
||||
/// <param name="minPremiereDate">Optional. The minimum premiere date. Format = ISO.</param>
|
||||
/// <param name="minDateLastSaved">Optional. The minimum last saved date. Format = ISO.</param>
|
||||
/// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
|
||||
/// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
|
||||
/// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</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 filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the 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="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This 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="isPlayed">Optional filter by items that are played, or not.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. 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="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
|
||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
||||
/// <param name="hasOfficialRating">Optional filter by items that have official ratings.</param>
|
||||
/// <param name="collapseBoxSetItems">Whether or not to hide items behind their boxsets.</param>
|
||||
/// <param name="minWidth">Optional. Filter by the minimum width of the item.</param>
|
||||
/// <param name="minHeight">Optional. Filter by the minimum height of the item.</param>
|
||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</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="studioIds">Optional. If specified, results will be filtered based on studio id. 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="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
[HttpGet("Items")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
[FromQuery] bool? hasThemeVideo,
|
||||
[FromQuery] bool? hasSubtitles,
|
||||
[FromQuery] bool? hasSpecialFeature,
|
||||
[FromQuery] bool? hasTrailer,
|
||||
[FromQuery] string? adjacentTo,
|
||||
[FromQuery] int? parentIndexNumber,
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
[FromQuery] double? minCommunityRating,
|
||||
[FromQuery] double? minCriticRating,
|
||||
[FromQuery] DateTime? minPremiereDate,
|
||||
[FromQuery] DateTime? minDateLastSaved,
|
||||
[FromQuery] DateTime? minDateLastSavedForUser,
|
||||
[FromQuery] DateTime? maxPremiereDate,
|
||||
[FromQuery] bool? hasOverview,
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
[FromQuery] bool? hasOfficialRating,
|
||||
[FromQuery] bool? collapseBoxSetItems,
|
||||
[FromQuery] int? minWidth,
|
||||
[FromQuery] int? minHeight,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
|
||||
|| includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentId = null;
|
||||
}
|
||||
|
||||
BaseItem? item = null;
|
||||
QueryResult<BaseItem> result;
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
item = _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
item ??= _libraryManager.GetUserRootFolder();
|
||||
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
if (folder is IHasCollectionType hasCollectionType
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
recursive = true;
|
||||
includeItemTypes = new[] { "Playlist" };
|
||||
}
|
||||
|
||||
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
|
||||
// Assume all folders inside an EnabledChannel are enabled
|
||||
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
|
||||
// Assume all items inside an EnabledChannel are enabled
|
||||
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
|
||||
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(item is UserRootFolder)
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
|
||||
{
|
||||
var query = new InternalItemsQuery(user!)
|
||||
{
|
||||
IsPlayed = isPlayed,
|
||||
MediaTypes = mediaTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
Recursive = recursive ?? false,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
IsFavorite = isFavorite,
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
IsMissing = isMissing,
|
||||
IsUnaired = isUnaired,
|
||||
CollapseBoxSetItems = collapseBoxSetItems,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
HasImdbId = hasImdbId,
|
||||
IsPlaceHolder = isPlaceHolder,
|
||||
IsLocked = isLocked,
|
||||
MinWidth = minWidth,
|
||||
MinHeight = minHeight,
|
||||
MaxWidth = maxWidth,
|
||||
MaxHeight = maxHeight,
|
||||
Is3D = is3D,
|
||||
HasTvdbId = hasTvdbId,
|
||||
HasTmdbId = hasTmdbId,
|
||||
HasOverview = hasOverview,
|
||||
HasOfficialRating = hasOfficialRating,
|
||||
HasParentalRating = hasParentalRating,
|
||||
HasSpecialFeature = hasSpecialFeature,
|
||||
HasSubtitles = hasSubtitles,
|
||||
HasThemeSong = hasThemeSong,
|
||||
HasThemeVideo = hasThemeVideo,
|
||||
HasTrailer = hasTrailer,
|
||||
IsHD = isHd,
|
||||
Is4K = is4K,
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
ArtistIds = artistIds,
|
||||
AlbumArtistIds = albumArtistIds,
|
||||
ContributingArtistIds = contributingArtistIds,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
ImageTypes = imageTypes,
|
||||
VideoTypes = videoTypes,
|
||||
AdjacentTo = adjacentTo,
|
||||
ItemIds = ids,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
MinCriticRating = minCriticRating,
|
||||
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
|
||||
ParentIndexNumber = parentIndexNumber,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
ExcludeItemIds = excludeItemIds,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
|
||||
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
|
||||
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
|
||||
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
|
||||
foreach (var filter in 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Series Status
|
||||
if (seriesStatus.Length != 0)
|
||||
{
|
||||
query.SeriesStatuses = seriesStatus;
|
||||
}
|
||||
|
||||
// ExcludeLocationTypes
|
||||
if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
|
||||
{
|
||||
query.IsVirtualItem = false;
|
||||
}
|
||||
|
||||
if (locationTypes.Length > 0 && locationTypes.Length < 4)
|
||||
{
|
||||
query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
|
||||
}
|
||||
|
||||
// Min official rating
|
||||
if (!string.IsNullOrWhiteSpace(minOfficialRating))
|
||||
{
|
||||
query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
|
||||
}
|
||||
|
||||
// Max official rating
|
||||
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
|
||||
{
|
||||
query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (artists.Length != 0)
|
||||
{
|
||||
query.ArtistIds = artists.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetArtist(i, new DtoOptions(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (excludeArtistIds.Length != 0)
|
||||
{
|
||||
query.ExcludeArtistIds = excludeArtistIds;
|
||||
}
|
||||
|
||||
if (albumIds.Length != 0)
|
||||
{
|
||||
query.AlbumIds = albumIds;
|
||||
}
|
||||
|
||||
// Albums
|
||||
if (albums.Length != 0)
|
||||
{
|
||||
query.AlbumIds = albums.SelectMany(i =>
|
||||
{
|
||||
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetStudio(i);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
// Apply default sorting if none requested
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
// Albums by artist
|
||||
if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
}
|
||||
|
||||
result = folder.GetItems(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
var itemsArray = folder.GetChildren(user, true);
|
||||
result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 };
|
||||
}
|
||||
|
||||
return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id supplied as query parameter.</param>
|
||||
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
|
||||
@@ -141,12 +580,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
[HttpGet("Items")]
|
||||
[HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
|
||||
[HttpGet("Users/{userId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||
[FromRoute] Guid? uId,
|
||||
[FromQuery] Guid? userId,
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
|
||||
[FromRoute] Guid userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
[FromQuery] bool? hasThemeVideo,
|
||||
@@ -158,8 +595,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery] string? locationTypes,
|
||||
[FromQuery] LocationType[] excludeLocationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
[FromQuery] double? minCommunityRating,
|
||||
@@ -172,42 +609,42 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] string? excludeItemIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? imageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? artists,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] string? artistIds,
|
||||
[FromQuery] string? albumArtistIds,
|
||||
[FromQuery] string? contributingArtistIds,
|
||||
[FromQuery] string? albums,
|
||||
[FromQuery] string? albumIds,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] string? videoTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
@@ -218,291 +655,96 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery] string? seriesStatus,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
// use user id route parameter over query parameter
|
||||
userId = uId ?? userId;
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parentId = null;
|
||||
}
|
||||
|
||||
BaseItem? item = null;
|
||||
QueryResult<BaseItem> result;
|
||||
if (!string.IsNullOrEmpty(parentId))
|
||||
{
|
||||
item = _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
item ??= _libraryManager.GetUserRootFolder();
|
||||
|
||||
if (!(item is Folder folder))
|
||||
{
|
||||
folder = _libraryManager.GetUserRootFolder();
|
||||
}
|
||||
|
||||
if (folder is IHasCollectionType hasCollectionType
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
recursive = true;
|
||||
includeItemTypes = "Playlist";
|
||||
}
|
||||
|
||||
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
|
||||
// Assume all folders inside an EnabledChannel are enabled
|
||||
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id)
|
||||
// Assume all items inside an EnabledChannel are enabled
|
||||
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId);
|
||||
|
||||
var collectionFolders = _libraryManager.GetCollectionFolders(item);
|
||||
foreach (var collectionFolder in collectionFolders)
|
||||
{
|
||||
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
|
||||
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
isInEnabledFolder = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(item is UserRootFolder)
|
||||
&& !isInEnabledFolder
|
||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||
&& !user.HasPermission(PermissionKind.EnableAllChannels))
|
||||
{
|
||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
|
||||
{
|
||||
var query = new InternalItemsQuery(user!)
|
||||
{
|
||||
IsPlayed = isPlayed,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
Recursive = recursive ?? false,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
IsFavorite = isFavorite,
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
IsMissing = isMissing,
|
||||
IsUnaired = isUnaired,
|
||||
CollapseBoxSetItems = collapseBoxSetItems,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
HasImdbId = hasImdbId,
|
||||
IsPlaceHolder = isPlaceHolder,
|
||||
IsLocked = isLocked,
|
||||
MinWidth = minWidth,
|
||||
MinHeight = minHeight,
|
||||
MaxWidth = maxWidth,
|
||||
MaxHeight = maxHeight,
|
||||
Is3D = is3D,
|
||||
HasTvdbId = hasTvdbId,
|
||||
HasTmdbId = hasTmdbId,
|
||||
HasOverview = hasOverview,
|
||||
HasOfficialRating = hasOfficialRating,
|
||||
HasParentalRating = hasParentalRating,
|
||||
HasSpecialFeature = hasSpecialFeature,
|
||||
HasSubtitles = hasSubtitles,
|
||||
HasThemeSong = hasThemeSong,
|
||||
HasThemeVideo = hasThemeVideo,
|
||||
HasTrailer = hasTrailer,
|
||||
IsHD = isHd,
|
||||
Is4K = is4K,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
ArtistIds = RequestHelpers.GetGuids(artistIds),
|
||||
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
|
||||
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
|
||||
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(),
|
||||
ImageTypes = RequestHelpers.Split(imageTypes, ',', true).Select(v => Enum.Parse<ImageType>(v, true)).ToArray(),
|
||||
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
|
||||
AdjacentTo = adjacentTo,
|
||||
ItemIds = RequestHelpers.GetGuids(ids),
|
||||
MinCommunityRating = minCommunityRating,
|
||||
MinCriticRating = minCriticRating,
|
||||
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
|
||||
ParentIndexNumber = parentIndexNumber,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
|
||||
MinDateLastSavedForUser = minDateLastSavedForUser?.ToUniversalTime(),
|
||||
MinPremiereDate = minPremiereDate?.ToUniversalTime(),
|
||||
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
|
||||
foreach (var filter in 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by Series Status
|
||||
if (!string.IsNullOrEmpty(seriesStatus))
|
||||
{
|
||||
query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
|
||||
}
|
||||
|
||||
// ExcludeLocationTypes
|
||||
if (excludeLocationTypes.Any(t => t == LocationType.Virtual))
|
||||
{
|
||||
query.IsVirtualItem = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(locationTypes))
|
||||
{
|
||||
var requestedLocationTypes = locationTypes.Split(',');
|
||||
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
|
||||
{
|
||||
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Min official rating
|
||||
if (!string.IsNullOrWhiteSpace(minOfficialRating))
|
||||
{
|
||||
query.MinParentalRating = _localization.GetRatingLevel(minOfficialRating);
|
||||
}
|
||||
|
||||
// Max official rating
|
||||
if (!string.IsNullOrWhiteSpace(maxOfficialRating))
|
||||
{
|
||||
query.MaxParentalRating = _localization.GetRatingLevel(maxOfficialRating);
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (!string.IsNullOrEmpty(artists))
|
||||
{
|
||||
query.ArtistIds = artists.Split('|').Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _libraryManager.GetArtist(i, new DtoOptions(false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}).Where(i => i != null).Select(i => i!.Id).ToArray();
|
||||
}
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrWhiteSpace(excludeArtistIds))
|
||||
{
|
||||
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(albumIds))
|
||||
{
|
||||
query.AlbumIds = RequestHelpers.GetGuids(albumIds);
|
||||
}
|
||||
|
||||
// Albums
|
||||
if (!string.IsNullOrEmpty(albums))
|
||||
{
|
||||
query.AlbumIds = albums.Split('|').SelectMany(i =>
|
||||
{
|
||||
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Apply default sorting if none requested
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
// Albums by artist
|
||||
if (query.ArtistIds.Length > 0 && query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], "MusicAlbum", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
query.OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.ProductionYear, SortOrder.Descending), new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
}
|
||||
|
||||
result = folder.GetItems(query);
|
||||
}
|
||||
else
|
||||
{
|
||||
var itemsArray = folder.GetChildren(user, true);
|
||||
result = new QueryResult<BaseItem> { Items = itemsArray, TotalRecordCount = itemsArray.Count, StartIndex = 0 };
|
||||
}
|
||||
|
||||
return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
|
||||
return GetItems(
|
||||
userId,
|
||||
maxOfficialRating,
|
||||
hasThemeSong,
|
||||
hasThemeVideo,
|
||||
hasSubtitles,
|
||||
hasSpecialFeature,
|
||||
hasTrailer,
|
||||
adjacentTo,
|
||||
parentIndexNumber,
|
||||
hasParentalRating,
|
||||
isHd,
|
||||
is4K,
|
||||
locationTypes,
|
||||
excludeLocationTypes,
|
||||
isMissing,
|
||||
isUnaired,
|
||||
minCommunityRating,
|
||||
minCriticRating,
|
||||
minPremiereDate,
|
||||
minDateLastSaved,
|
||||
minDateLastSavedForUser,
|
||||
maxPremiereDate,
|
||||
hasOverview,
|
||||
hasImdbId,
|
||||
hasTmdbId,
|
||||
hasTvdbId,
|
||||
excludeItemIds,
|
||||
startIndex,
|
||||
limit,
|
||||
recursive,
|
||||
searchTerm,
|
||||
sortOrder,
|
||||
parentId,
|
||||
fields,
|
||||
excludeItemTypes,
|
||||
includeItemTypes,
|
||||
filters,
|
||||
isFavorite,
|
||||
mediaTypes,
|
||||
imageTypes,
|
||||
sortBy,
|
||||
isPlayed,
|
||||
genres,
|
||||
officialRatings,
|
||||
tags,
|
||||
years,
|
||||
enableUserData,
|
||||
imageTypeLimit,
|
||||
enableImageTypes,
|
||||
person,
|
||||
personIds,
|
||||
personTypes,
|
||||
studios,
|
||||
artists,
|
||||
excludeArtistIds,
|
||||
artistIds,
|
||||
albumArtistIds,
|
||||
contributingArtistIds,
|
||||
albums,
|
||||
albumIds,
|
||||
ids,
|
||||
videoTypes,
|
||||
minOfficialRating,
|
||||
isLocked,
|
||||
isPlaceHolder,
|
||||
hasOfficialRating,
|
||||
collapseBoxSetItems,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
is3D,
|
||||
seriesStatus,
|
||||
nameStartsWithOrGreater,
|
||||
nameStartsWith,
|
||||
nameLessThan,
|
||||
studioIds,
|
||||
genreIds,
|
||||
enableTotalRecordCount,
|
||||
enableImages);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -513,13 +755,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="limit">The item limit.</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 delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</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="mediaTypes">Optional. Filter by MediaType. 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="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||
/// <response code="200">Items returned.</response>
|
||||
@@ -532,20 +774,19 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -570,13 +811,13 @@ namespace Jellyfin.Api.Controllers
|
||||
ParentId = parentIdGuid,
|
||||
Recursive = true,
|
||||
DtoOptions = dtoOptions,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
MediaTypes = mediaTypes,
|
||||
IsVirtualItem = false,
|
||||
CollapseBoxSetItems = false,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
AncestorIds = ancestorIds,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
SearchTerm = searchTerm
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.LibraryDtos;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Progress;
|
||||
@@ -361,15 +362,14 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult DeleteItems([FromQuery] string? ids)
|
||||
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ids))
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var itemIds = RequestHelpers.Split(ids, ',', true);
|
||||
foreach (var i in itemIds)
|
||||
foreach (var i in ids)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(i);
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
@@ -455,7 +455,7 @@ namespace Jellyfin.Api.Controllers
|
||||
: null;
|
||||
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
BaseItem parent = item.GetParent();
|
||||
BaseItem? parent = item.GetParent();
|
||||
|
||||
while (parent != null)
|
||||
{
|
||||
@@ -466,7 +466,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
|
||||
|
||||
parent = parent.GetParent();
|
||||
parent = parent?.GetParent();
|
||||
}
|
||||
|
||||
return baseItemDtos;
|
||||
@@ -680,20 +680,20 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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, TrailerUrls.</param>
|
||||
/// <response code="200">Similar items returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> containing the similar items.</returns>
|
||||
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists2")]
|
||||
[HttpGet("Artists/{itemId}/Similar", Name = "GetSimilarArtists")]
|
||||
[HttpGet("Items/{itemId}/Similar")]
|
||||
[HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums2")]
|
||||
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows2")]
|
||||
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies2")]
|
||||
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers2")]
|
||||
[HttpGet("Albums/{itemId}/Similar", Name = "GetSimilarAlbums")]
|
||||
[HttpGet("Shows/{itemId}/Similar", Name = "GetSimilarShows")]
|
||||
[HttpGet("Movies/{itemId}/Similar", Name = "GetSimilarMovies")]
|
||||
[HttpGet("Trailers/{itemId}/Similar", Name = "GetSimilarTrailers")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||
{
|
||||
var item = itemId.Equals(Guid.Empty)
|
||||
? (!userId.Equals(Guid.Empty)
|
||||
@@ -701,33 +701,71 @@ namespace Jellyfin.Api.Controllers
|
||||
: _libraryManager.RootFolder)
|
||||
: _libraryManager.GetItemById(itemId);
|
||||
|
||||
var program = item as IHasProgramAttributes;
|
||||
var isMovie = item is MediaBrowser.Controller.Entities.Movies.Movie || (program != null && program.IsMovie) || item is Trailer;
|
||||
if (program != null && program.IsSeries)
|
||||
{
|
||||
return GetSimilarItemsResult(
|
||||
item,
|
||||
excludeArtistIds,
|
||||
userId,
|
||||
limit,
|
||||
fields,
|
||||
new[] { nameof(Series) },
|
||||
false);
|
||||
}
|
||||
|
||||
if (item is MediaBrowser.Controller.Entities.TV.Episode || (item is IItemByName && !(item is MusicArtist)))
|
||||
if (item is Episode || (item is IItemByName && !(item is MusicArtist)))
|
||||
{
|
||||
return new QueryResult<BaseItemDto>();
|
||||
}
|
||||
|
||||
return GetSimilarItemsResult(
|
||||
item,
|
||||
excludeArtistIds,
|
||||
userId,
|
||||
limit,
|
||||
fields,
|
||||
new[] { item.GetType().Name },
|
||||
isMovie);
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request);
|
||||
|
||||
var program = item as IHasProgramAttributes;
|
||||
bool? isMovie = item is Movie || (program != null && program.IsMovie) || item is Trailer;
|
||||
bool? isSeries = item is Series || (program != null && program.IsSeries);
|
||||
|
||||
var includeItemTypes = new List<string>();
|
||||
if (isMovie.Value)
|
||||
{
|
||||
includeItemTypes.Add(nameof(Movie));
|
||||
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
|
||||
{
|
||||
includeItemTypes.Add(nameof(Trailer));
|
||||
includeItemTypes.Add(nameof(LiveTvProgram));
|
||||
}
|
||||
}
|
||||
else if (isSeries.Value)
|
||||
{
|
||||
includeItemTypes.Add(nameof(Series));
|
||||
}
|
||||
else
|
||||
{
|
||||
// For non series and movie types these columns are typically null
|
||||
isSeries = null;
|
||||
isMovie = null;
|
||||
includeItemTypes.Add(item.GetType().Name);
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Limit = limit,
|
||||
IncludeItemTypes = includeItemTypes.ToArray(),
|
||||
IsMovie = isMovie,
|
||||
IsSeries = isSeries,
|
||||
SimilarTo = item,
|
||||
DtoOptions = dtoOptions,
|
||||
EnableTotalRecordCount = !isMovie ?? true,
|
||||
EnableGroupByMetadataKey = isMovie ?? false,
|
||||
MinSimilarityScore = 2 // A remnant from album/artist scoring
|
||||
};
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (excludeArtistIds.Length != 0)
|
||||
{
|
||||
query.ExcludeArtistIds = excludeArtistIds;
|
||||
}
|
||||
|
||||
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
|
||||
|
||||
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnList,
|
||||
TotalRecordCount = itemsResult.Count
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -854,7 +892,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return _libraryManager.GetItemsResult(query).TotalRecordCount;
|
||||
}
|
||||
|
||||
private BaseItem TranslateParentItem(BaseItem item, User user)
|
||||
private BaseItem? TranslateParentItem(BaseItem item, User user)
|
||||
{
|
||||
return item.GetParent() is AggregateFolder
|
||||
? _libraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||
@@ -880,75 +918,6 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private QueryResult<BaseItemDto> GetSimilarItemsResult(
|
||||
BaseItem item,
|
||||
string? excludeArtistIds,
|
||||
Guid? userId,
|
||||
int? limit,
|
||||
string? fields,
|
||||
string[] includeItemTypes,
|
||||
bool isMovie)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
.AddClientFields(Request);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Limit = limit,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
IsMovie = isMovie,
|
||||
SimilarTo = item,
|
||||
DtoOptions = dtoOptions,
|
||||
EnableTotalRecordCount = !isMovie,
|
||||
EnableGroupByMetadataKey = isMovie
|
||||
};
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrEmpty(excludeArtistIds))
|
||||
{
|
||||
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
|
||||
}
|
||||
|
||||
List<BaseItem> itemsResult;
|
||||
|
||||
if (isMovie)
|
||||
{
|
||||
var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) };
|
||||
if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions)
|
||||
{
|
||||
itemTypes.Add(nameof(Trailer));
|
||||
itemTypes.Add(nameof(LiveTvProgram));
|
||||
}
|
||||
|
||||
query.IncludeItemTypes = itemTypes.ToArray();
|
||||
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
|
||||
}
|
||||
else if (item is MusicArtist)
|
||||
{
|
||||
query.IncludeItemTypes = Array.Empty<string>();
|
||||
|
||||
itemsResult = _libraryManager.GetArtists(query).Items.Select(i => i.Item1).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
itemsResult = _libraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
var returnList = _dtoService.GetBaseItemDtos(itemsResult, dtoOptions, user);
|
||||
|
||||
var result = new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = returnList,
|
||||
TotalRecordCount = itemsResult.Count
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string[] GetRepresentativeItemTypes(string? contentType)
|
||||
{
|
||||
return contentType switch
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.LibraryStructureDto;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -75,7 +76,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult> AddVirtualFolder(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? collectionType,
|
||||
[FromQuery] string[] paths,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
||||
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
||||
[FromQuery] bool refreshLibrary = false)
|
||||
{
|
||||
|
||||
@@ -14,9 +14,9 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.LiveTvDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -26,6 +26,7 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="sortBy">Optional. Key to sort by.</param>
|
||||
/// <param name="sortOrder">Optional. Sort order.</param>
|
||||
@@ -145,16 +146,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isDisliked,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] SortOrder? sortOrder,
|
||||
[FromQuery] bool enableFavoriteSorting = false,
|
||||
[FromQuery] bool addCurrentProgram = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -174,7 +174,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsNews = isNews,
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
SortBy = RequestHelpers.Split(sortBy, ',', true),
|
||||
SortBy = sortBy,
|
||||
SortOrder = sortOrder ?? SortOrder.Ascending,
|
||||
AddCurrentProgram = addCurrentProgram
|
||||
},
|
||||
@@ -238,7 +238,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="isMovie">Optional. Filter for movies.</param>
|
||||
/// <param name="isSeries">Optional. Filter for series.</param>
|
||||
@@ -262,8 +262,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? seriesTimerId,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
@@ -273,8 +273,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isLibraryItem,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -295,7 +294,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
IsLibraryItem = isLibraryItem,
|
||||
Fields = RequestHelpers.GetItemFields(fields),
|
||||
Fields = fields,
|
||||
ImageTypeLimit = imageTypeLimit,
|
||||
EnableImages = enableImages
|
||||
}, dtoOptions);
|
||||
@@ -315,7 +314,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="enableTotalRecordCount">Optional. Return total record count.</param>
|
||||
/// <response code="200">Live tv recordings returned.</response>
|
||||
@@ -349,8 +348,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? seriesTimerId,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -529,7 +528,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||
/// <param name="seriesTimerId">Optional. Filter by series timer id.</param>
|
||||
/// <param name="librarySeriesId">Optional. Filter by library series id.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableTotalRecordCount">Retrieve total record count.</param>
|
||||
/// <response code="200">Live tv epgs returned.</response>
|
||||
/// <returns>
|
||||
@@ -539,7 +538,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||
[FromQuery] string? channelIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] DateTime? minStartDate,
|
||||
[FromQuery] bool? hasAired,
|
||||
@@ -556,15 +555,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] string? seriesTimerId,
|
||||
[FromQuery] Guid? librarySeriesId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
@@ -573,8 +572,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ChannelIds = RequestHelpers.Split(channelIds, ',', true)
|
||||
.Select(i => new Guid(i)).ToArray(),
|
||||
ChannelIds = channelIds,
|
||||
HasAired = hasAired,
|
||||
IsAiring = isAiring,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
@@ -591,8 +589,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
SeriesTimerId = seriesTimerId,
|
||||
Genres = RequestHelpers.Split(genres, ',', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||
Genres = genres,
|
||||
GenreIds = genreIds
|
||||
};
|
||||
|
||||
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
|
||||
@@ -605,8 +603,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -629,8 +626,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
|
||||
.Select(i => new Guid(i)).ToArray(),
|
||||
ChannelIds = body.ChannelIds,
|
||||
HasAired = body.HasAired,
|
||||
IsAiring = body.IsAiring,
|
||||
EnableTotalRecordCount = body.EnableTotalRecordCount,
|
||||
@@ -647,8 +643,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IsKids = body.IsKids,
|
||||
IsSports = body.IsSports,
|
||||
SeriesTimerId = body.SeriesTimerId,
|
||||
Genres = RequestHelpers.Split(body.Genres, ',', true),
|
||||
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
|
||||
Genres = body.Genres,
|
||||
GenreIds = body.GenreIds
|
||||
};
|
||||
|
||||
if (!body.LibrarySeriesId.Equals(Guid.Empty))
|
||||
@@ -661,8 +657,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(body.Fields)
|
||||
var dtoOptions = new DtoOptions { Fields = body.Fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
|
||||
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
|
||||
@@ -684,7 +679,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="genreIds">The genres to return guide information for.</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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="enableUserData">Optional. include user data.</param>
|
||||
/// <param name="enableTotalRecordCount">Retrieve total record count.</param>
|
||||
/// <response code="200">Recommended epgs returned.</response>
|
||||
@@ -704,9 +699,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? isSports,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
@@ -725,11 +720,10 @@ namespace Jellyfin.Api.Controllers
|
||||
IsNews = isNews,
|
||||
IsSports = isSports,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||
GenreIds = genreIds
|
||||
};
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
return _liveTvManager.GetRecommendedPrograms(query, dtoOptions, CancellationToken.None);
|
||||
@@ -1020,7 +1014,9 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!string.IsNullOrEmpty(pw))
|
||||
{
|
||||
using var sha = SHA1.Create();
|
||||
listingsProviderInfo.Password = Hex.Encode(sha.ComputeHash(Encoding.UTF8.GetBytes(pw)));
|
||||
// TODO: remove ToLower when Convert.ToHexString supports lowercase
|
||||
// Schedules Direct requires the hex to be lowercase
|
||||
listingsProviderInfo.Password = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
|
||||
@@ -1076,7 +1072,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var client = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
// https://json.schedulesdirect.org/20141201/available/countries
|
||||
// Can't dispose the response as it's required up the call chain.
|
||||
var response = await client.GetAsync("https://json.schedulesdirect.org/20141201/available/countries")
|
||||
var response = await client.GetAsync(new Uri("https://json.schedulesdirect.org/20141201/available/countries"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return File(await response.Content.ReadAsStreamAsync().ConfigureAwait(false), MediaTypeNames.Application.Json);
|
||||
@@ -1158,7 +1154,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="newDevicesOnly">Only discover new tuners.</param>
|
||||
/// <response code="200">Tuners returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
|
||||
[HttpGet("Tuners/Discvover")]
|
||||
[HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
|
||||
[HttpGet("Tuners/Discover")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
|
||||
@@ -1219,11 +1216,8 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await using var memoryStream = new MemoryStream();
|
||||
await new ProgressiveFileCopier(liveStreamInfo, null, _transcodingJobHelper, CancellationToken.None)
|
||||
.WriteToAsync(memoryStream, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
return File(memoryStream, MimeTypes.GetMimeType("file." + container));
|
||||
var liveStream = new ProgressiveFileStream(liveStreamInfo.GetFilePath(), null, _transcodingJobHelper);
|
||||
return new FileStreamResult(liveStream, MimeTypes.GetMimeType("file." + container));
|
||||
}
|
||||
|
||||
private void AssertUserCanManageLiveTv()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@@ -8,7 +8,6 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.MediaInfoDtos;
|
||||
using Jellyfin.Api.Models.VideoDtos;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -81,6 +80,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets live playback media info for an item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||
/// </remarks>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="maxStreamingBitrate">The maximum streaming bitrate.</param>
|
||||
@@ -90,13 +92,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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>
|
||||
/// <param name="playbackInfoDto">The playback info.</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")]
|
||||
@@ -104,25 +106,24 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromBody] DeviceProfileDto? deviceProfile,
|
||||
[FromQuery] bool autoOpenLiveStream = false,
|
||||
[FromQuery] bool enableDirectPlay = true,
|
||||
[FromQuery] bool enableDirectStream = true,
|
||||
[FromQuery] bool enableTranscoding = true,
|
||||
[FromQuery] bool allowVideoStreamCopy = true,
|
||||
[FromQuery] bool allowAudioStreamCopy = true)
|
||||
[FromQuery] bool? autoOpenLiveStream,
|
||||
[FromQuery] bool? enableDirectPlay,
|
||||
[FromQuery] bool? enableDirectStream,
|
||||
[FromQuery] bool? enableTranscoding,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromBody] PlaybackInfoDto? playbackInfoDto)
|
||||
{
|
||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||
|
||||
var profile = deviceProfile?.DeviceProfile;
|
||||
|
||||
var profile = playbackInfoDto?.DeviceProfile;
|
||||
_logger.LogInformation("GetPostedPlaybackInfo profile: {@Profile}", profile);
|
||||
|
||||
if (profile == null)
|
||||
@@ -134,6 +135,23 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// Copy params from posted body
|
||||
// TODO clean up when breaking API compatibility.
|
||||
userId ??= playbackInfoDto?.UserId;
|
||||
maxStreamingBitrate ??= playbackInfoDto?.MaxStreamingBitrate;
|
||||
startTimeTicks ??= playbackInfoDto?.StartTimeTicks;
|
||||
audioStreamIndex ??= playbackInfoDto?.AudioStreamIndex;
|
||||
subtitleStreamIndex ??= playbackInfoDto?.SubtitleStreamIndex;
|
||||
maxAudioChannels ??= playbackInfoDto?.MaxAudioChannels;
|
||||
mediaSourceId ??= playbackInfoDto?.MediaSourceId;
|
||||
liveStreamId ??= playbackInfoDto?.LiveStreamId;
|
||||
autoOpenLiveStream ??= playbackInfoDto?.AutoOpenLiveStream ?? false;
|
||||
enableDirectPlay ??= playbackInfoDto?.EnableDirectPlay ?? true;
|
||||
enableDirectStream ??= playbackInfoDto?.EnableDirectStream ?? true;
|
||||
enableTranscoding ??= playbackInfoDto?.EnableTranscoding ?? true;
|
||||
allowVideoStreamCopy ??= playbackInfoDto?.AllowVideoStreamCopy ?? true;
|
||||
allowAudioStreamCopy ??= playbackInfoDto?.AllowAudioStreamCopy ?? true;
|
||||
|
||||
var info = await _mediaInfoHelper.GetPlaybackInfo(
|
||||
itemId,
|
||||
userId,
|
||||
@@ -161,18 +179,18 @@ namespace Jellyfin.Api.Controllers
|
||||
maxAudioChannels,
|
||||
info!.PlaySessionId!,
|
||||
userId ?? Guid.Empty,
|
||||
enableDirectPlay,
|
||||
enableDirectStream,
|
||||
enableTranscoding,
|
||||
allowVideoStreamCopy,
|
||||
allowAudioStreamCopy,
|
||||
enableDirectPlay.Value,
|
||||
enableDirectStream.Value,
|
||||
enableTranscoding.Value,
|
||||
allowVideoStreamCopy.Value,
|
||||
allowAudioStreamCopy.Value,
|
||||
Request.HttpContext.GetNormalizedRemoteIp());
|
||||
}
|
||||
|
||||
_mediaInfoHelper.SortMediaSources(info, maxStreamingBitrate);
|
||||
}
|
||||
|
||||
if (autoOpenLiveStream)
|
||||
if (autoOpenLiveStream.Value)
|
||||
{
|
||||
var mediaSource = string.IsNullOrWhiteSpace(mediaSourceId) ? info.MediaSources[0] : info.MediaSources.FirstOrDefault(i => string.Equals(i.Id, mediaSourceId, StringComparison.Ordinal));
|
||||
|
||||
@@ -183,9 +201,9 @@ namespace Jellyfin.Api.Controllers
|
||||
new LiveStreamRequest
|
||||
{
|
||||
AudioStreamIndex = audioStreamIndex,
|
||||
DeviceProfile = deviceProfile?.DeviceProfile,
|
||||
EnableDirectPlay = enableDirectPlay,
|
||||
EnableDirectStream = enableDirectStream,
|
||||
DeviceProfile = playbackInfoDto?.DeviceProfile,
|
||||
EnableDirectPlay = enableDirectPlay.Value,
|
||||
EnableDirectStream = enableDirectStream.Value,
|
||||
ItemId = itemId,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxStreamingBitrate = maxStreamingBitrate,
|
||||
@@ -234,7 +252,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? openToken,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
@@ -65,15 +66,14 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<IEnumerable<RecommendationDto>> GetMovieRecommendations(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] int categoryLimit = 5,
|
||||
[FromQuery] int itemLimit = 8)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request);
|
||||
|
||||
var categories = new List<RecommendationDto>();
|
||||
@@ -85,8 +85,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IncludeItemTypes = new[]
|
||||
{
|
||||
nameof(Movie),
|
||||
// typeof(Trailer).Name,
|
||||
// typeof(LiveTvProgram).Name
|
||||
// nameof(Trailer),
|
||||
// nameof(LiveTvProgram)
|
||||
},
|
||||
// IsMovie = true
|
||||
OrderBy = new[] { ItemSortBy.DatePlayed, ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -47,30 +48,16 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Gets all music genres 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="fields">Optional. Specify additional fields of information to return in the output.</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.</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>
|
||||
@@ -80,31 +67,18 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">Music genres returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the queryresult of music genres.</returns>
|
||||
[HttpGet]
|
||||
[Obsolete("Use GetGenres instead")]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetMusicGenres(
|
||||
[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] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@@ -112,45 +86,24 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
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
|
||||
@@ -168,87 +121,10 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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.GetMusicGenres(query);
|
||||
|
||||
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
|
||||
};
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -263,7 +139,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
var dtoOptions = new DtoOptions().AddClientFields(Request);
|
||||
|
||||
MusicGenre item;
|
||||
MusicGenre? item;
|
||||
|
||||
if (genreName.IndexOf(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
@@ -284,7 +160,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||
}
|
||||
|
||||
private T GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
||||
private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions)
|
||||
where T : BaseItem, new()
|
||||
{
|
||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
||||
|
||||
@@ -54,6 +54,11 @@ namespace Jellyfin.Api.Controllers
|
||||
string.IsNullOrEmpty(assemblyGuid) ? default : Guid.Parse(assemblyGuid))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -94,7 +99,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(repositoryUrl))
|
||||
{
|
||||
packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))
|
||||
packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -149,12 +154,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="repositoryInfos">The list of package repositories.</param>
|
||||
/// <response code="204">Package repositories saved.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpOptions("Repositories")]
|
||||
[HttpPost("Repositories")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
|
||||
{
|
||||
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||
_serverConfigurationManager.SaveConfiguration();
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -26,6 +27,7 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PersonsController"/> class.
|
||||
@@ -33,221 +35,81 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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>
|
||||
/// <param name="userDataManager">Instance of the <see cref="IUserDataManager"/> interface.</param>
|
||||
public PersonsController(
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService,
|
||||
IUserManager userManager)
|
||||
IUserManager userManager,
|
||||
IUserDataManager userDataManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
_userManager = userManager;
|
||||
_userDataManager = userDataManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all persons from a given item, folder, or the entire library.
|
||||
/// Gets all persons.
|
||||
/// </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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not. userId is required.</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="excludePersonTypes">Optional. If specified results will be filtered to exclude those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="personTypes">Optional. If specified results will be filtered to include only those containing the specified PersonType. Allows multiple, comma-delimited.</param>
|
||||
/// <param name="appearsInItemId">Optional. If specified, person results will be filtered on items related to said persons.</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] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] 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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery] string? appearsInItemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
else
|
||||
{
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
|
||||
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
|
||||
{
|
||||
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 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;
|
||||
PersonTypes = personTypes,
|
||||
ExcludePersonTypes = excludePersonTypes,
|
||||
NameContains = searchTerm,
|
||||
User = user,
|
||||
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
|
||||
AppearsInItemId = string.IsNullOrEmpty(appearsInItemId) ? Guid.Empty : Guid.Parse(appearsInItemId),
|
||||
Limit = limit ?? 0
|
||||
});
|
||||
|
||||
return new QueryResult<BaseItemDto>
|
||||
{
|
||||
Items = dtos.ToArray(),
|
||||
TotalRecordCount = result.TotalRecordCount
|
||||
Items = peopleItems.Select(person => _dtoService.GetItemByNameDto(person, dtoOptions, null, user)).ToArray(),
|
||||
TotalRecordCount = peopleItems.Count
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.PlaylistDtos;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Playlists;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -61,11 +63,10 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
||||
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
|
||||
{
|
||||
Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
|
||||
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||
{
|
||||
Name = createPlaylistRequest.Name,
|
||||
ItemIdList = idGuidArray,
|
||||
ItemIdList = createPlaylistRequest.Ids,
|
||||
UserId = createPlaylistRequest.UserId,
|
||||
MediaType = createPlaylistRequest.MediaType
|
||||
}).ConfigureAwait(false);
|
||||
@@ -85,10 +86,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddToPlaylist(
|
||||
[FromRoute, Required] Guid playlistId,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery] Guid? userId)
|
||||
{
|
||||
await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
|
||||
await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -120,9 +121,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||
[HttpDelete("{playlistId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
|
||||
public async Task<ActionResult> RemoveFromPlaylist(
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
|
||||
{
|
||||
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
|
||||
await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -133,7 +136,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="userId">User id.</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="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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -147,11 +150,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, Required] Guid userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
|
||||
if (playlist == null)
|
||||
@@ -175,8 +178,7 @@ namespace Jellyfin.Api.Controllers
|
||||
items = items.Take(limit.Value).ToArray();
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<UserItemDataDto> MarkPlayedItem(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] DateTime? datePlayed)
|
||||
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
@@ -157,9 +157,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] string imageUrl)
|
||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
|
||||
{
|
||||
var urlHash = imageUrl.GetMD5();
|
||||
var urlHash = imageUrl.ToString().GetMD5();
|
||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
||||
|
||||
string? contentPath = null;
|
||||
@@ -245,17 +245,25 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="urlHash">The URL hash.</param>
|
||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
|
||||
private async Task DownloadImage(Uri url, Guid urlHash, string pointerCachePath)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient(NamedClient.Default);
|
||||
using var response = await httpClient.GetAsync(url).ConfigureAwait(false);
|
||||
var ext = response.Content.Headers.ContentType.MediaType.Split('/').Last();
|
||||
if (response.Content.Headers.ContentType?.MediaType == null)
|
||||
{
|
||||
throw new ResourceNotFoundException(nameof(response.Content.Headers.ContentType));
|
||||
}
|
||||
|
||||
var ext = response.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
|
||||
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||
Directory.CreateDirectory(fullCacheDirectory);
|
||||
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
||||
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
|
||||
|
||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||
Directory.CreateDirectory(pointerCacheDirectory);
|
||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>The list of scheduled tasks.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<IScheduledTaskWorker> GetTasks(
|
||||
public IEnumerable<TaskInfo> GetTasks(
|
||||
[FromQuery] bool? isHidden,
|
||||
[FromQuery] bool? isEnabled)
|
||||
{
|
||||
@@ -57,7 +57,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
yield return task;
|
||||
yield return ScheduledTaskHelpers.GetTaskInfo(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery, Required] string searchTerm,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
@@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
|
||||
IncludeStudios = includeStudios,
|
||||
StartIndex = startIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
ParentId = parentId,
|
||||
|
||||
IsKids = isKids,
|
||||
@@ -260,7 +261,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
private T GetParentWithImage<T>(BaseItem item, ImageType type)
|
||||
private T? GetParentWithImage<T>(BaseItem item, ImageType type)
|
||||
where T : BaseItem
|
||||
{
|
||||
return item.GetParents().OfType<T>().FirstOrDefault(i => i.HasImage(type));
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Threading;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.SessionDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -160,12 +161,12 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult Play(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] PlayCommand playCommand,
|
||||
[FromQuery, Required] string itemIds,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
||||
[FromQuery] long? startPositionTicks)
|
||||
{
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = RequestHelpers.GetGuids(itemIds),
|
||||
ItemIds = itemIds,
|
||||
StartPositionTicks = startPositionTicks,
|
||||
PlayCommand = playCommand
|
||||
};
|
||||
@@ -378,8 +379,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromQuery] string? playableMediaTypes,
|
||||
[FromQuery] GeneralCommandType[] supportedCommands,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
|
||||
[FromQuery] bool supportsMediaControl = false,
|
||||
[FromQuery] bool supportsSync = false,
|
||||
[FromQuery] bool supportsPersistentIdentifier = true)
|
||||
@@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
_sessionManager.ReportCapabilities(id, new ClientCapabilities
|
||||
{
|
||||
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
|
||||
PlayableMediaTypes = playableMediaTypes,
|
||||
SupportedCommands = supportedCommands,
|
||||
SupportsMediaControl = supportsMediaControl,
|
||||
SupportsSync = supportsSync,
|
||||
@@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostFullCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromBody, Required] ClientCapabilities capabilities)
|
||||
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
}
|
||||
|
||||
_sessionManager.ReportCapabilities(id, capabilities);
|
||||
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.StartupDtos;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -72,9 +73,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
|
||||
{
|
||||
_config.Configuration.UICulture = startupConfiguration.UICulture;
|
||||
_config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode;
|
||||
_config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage;
|
||||
_config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
|
||||
_config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
|
||||
_config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
|
||||
_config.SaveConfiguration();
|
||||
return NoContent();
|
||||
}
|
||||
@@ -89,9 +90,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult SetRemoteAccess([FromBody, Required] StartupRemoteAccessDto startupRemoteAccessDto)
|
||||
{
|
||||
_config.Configuration.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
|
||||
_config.Configuration.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
|
||||
_config.SaveConfiguration();
|
||||
NetworkConfiguration settings = _config.GetNetworkConfiguration();
|
||||
settings.EnableRemoteAccess = startupRemoteAccessDto.EnableRemoteAccess;
|
||||
settings.EnableUPnP = startupRemoteAccessDto.EnableAutomaticPortMapping;
|
||||
_config.SaveConfiguration("network", settings);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -45,30 +46,17 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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.</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>
|
||||
@@ -80,30 +68,17 @@ namespace Jellyfin.Api.Controllers
|
||||
[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] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[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, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@@ -111,49 +86,24 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages = true,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
User? user = null;
|
||||
BaseItem parentItem;
|
||||
User? user = userId.HasValue && userId != Guid.Empty ? _userManager.GetUserById(userId.Value) : null;
|
||||
|
||||
if (userId.HasValue && !userId.Equals(Guid.Empty))
|
||||
{
|
||||
user = _userManager.GetUserById(userId.Value);
|
||||
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 parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
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
|
||||
@@ -171,84 +121,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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
|
||||
};
|
||||
var result = _libraryManager.GetStudios(query);
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -11,6 +11,9 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Models.SubtitleDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -21,6 +24,7 @@ using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using MediaBrowser.Model.Subtitles;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -34,6 +38,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Route("")]
|
||||
public class SubtitleController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ISubtitleManager _subtitleManager;
|
||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||
@@ -46,6 +51,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
|
||||
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
|
||||
@@ -55,6 +61,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
|
||||
public SubtitleController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILibraryManager libraryManager,
|
||||
ISubtitleManager subtitleManager,
|
||||
ISubtitleEncoder subtitleEncoder,
|
||||
@@ -64,6 +71,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IAuthorizationContext authContext,
|
||||
ILogger<SubtitleController> logger)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_libraryManager = libraryManager;
|
||||
_subtitleManager = subtitleManager;
|
||||
_subtitleEncoder = subtitleEncoder;
|
||||
@@ -185,7 +193,6 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
@@ -196,7 +203,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false,
|
||||
[FromRoute] long startPositionTicks = 0)
|
||||
[FromQuery] long startPositionTicks = 0)
|
||||
{
|
||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -241,6 +248,43 @@ namespace Jellyfin.Api.Controllers
|
||||
MimeTypes.GetMimeType("file." + format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets subtitles in a specified format.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
||||
/// <param name="format">The format of the returned subtitle.</param>
|
||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public Task<ActionResult> GetSubtitleWithTicks(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
[FromRoute, Required] int index,
|
||||
[FromRoute, Required] long startPositionTicks,
|
||||
[FromRoute, Required] string format,
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false)
|
||||
{
|
||||
return GetSubtitle(
|
||||
itemId,
|
||||
mediaSourceId,
|
||||
index,
|
||||
format,
|
||||
endPositionTicks,
|
||||
copyTimestamps,
|
||||
addVttTimeMap,
|
||||
startPositionTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an HLS subtitle playlist.
|
||||
/// </summary>
|
||||
@@ -319,6 +363,34 @@ namespace Jellyfin.Api.Controllers
|
||||
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload an external subtitle file.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item the subtitle belongs to.</param>
|
||||
/// <param name="body">The request body.</param>
|
||||
/// <response code="204">Subtitle uploaded.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UploadSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var data = Convert.FromBase64String(body.Data);
|
||||
await using var memoryStream = new MemoryStream(data);
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
new SubtitleResponse
|
||||
{
|
||||
Format = body.Format,
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
Stream = memoryStream
|
||||
}).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a subtitle in the specified format.
|
||||
/// </summary>
|
||||
@@ -351,5 +423,96 @@ namespace Jellyfin.Api.Controllers
|
||||
copyTimestamps,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of available fallback font files.
|
||||
/// </summary>
|
||||
/// <response code="200">Information retrieved.</response>
|
||||
/// <returns>An array of <see cref="FontFile"/> with the available font files.</returns>
|
||||
[HttpGet("FallbackFont/Fonts")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public IEnumerable<FontFile> GetFallbackFontList()
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var fallbackFontPath = encodingOptions.FallbackFontPath;
|
||||
|
||||
if (!string.IsNullOrEmpty(fallbackFontPath))
|
||||
{
|
||||
var files = _fileSystem.GetFiles(fallbackFontPath, new[] { ".woff", ".woff2", ".ttf", ".otf" }, false, false);
|
||||
var fontFiles = files
|
||||
.Select(i => new FontFile
|
||||
{
|
||||
Name = i.Name,
|
||||
Size = i.Length,
|
||||
DateCreated = _fileSystem.GetCreationTimeUtc(i),
|
||||
DateModified = _fileSystem.GetLastWriteTimeUtc(i)
|
||||
})
|
||||
.OrderBy(i => i.Size)
|
||||
.ThenBy(i => i.Name)
|
||||
.ThenByDescending(i => i.DateModified)
|
||||
.ThenByDescending(i => i.DateCreated);
|
||||
// max total size 20M
|
||||
const int MaxSize = 20971520;
|
||||
var sizeCounter = 0L;
|
||||
foreach (var fontFile in fontFiles)
|
||||
{
|
||||
sizeCounter += fontFile.Size;
|
||||
if (sizeCounter >= MaxSize)
|
||||
{
|
||||
_logger.LogWarning("Some fonts will not be sent due to size limitations");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return fontFile;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The path of fallback font folder has not been set");
|
||||
encodingOptions.EnableFallbackFont = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a fallback font file.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the fallback font file to get.</param>
|
||||
/// <response code="200">Fallback font file retrieved.</response>
|
||||
/// <returns>The fallback font file.</returns>
|
||||
[HttpGet("FallbackFont/Fonts/{name}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("font/*")]
|
||||
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var fallbackFontPath = encodingOptions.FallbackFontPath;
|
||||
|
||||
if (!string.IsNullOrEmpty(fallbackFontPath))
|
||||
{
|
||||
var fontFile = _fileSystem.GetFiles(fallbackFontPath)
|
||||
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
var fileSize = fontFile?.Length;
|
||||
|
||||
if (fontFile != null && fileSize != null && fileSize > 0)
|
||||
{
|
||||
_logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize);
|
||||
return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The selected font is null or empty");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("The path of fallback font folder has not been set");
|
||||
encodingOptions.EnableFallbackFont = false;
|
||||
}
|
||||
|
||||
// returning HTTP 204 will break the SubtitlesOctopus
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
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;
|
||||
|
||||
@@ -18,6 +21,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// The suggestions controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class SuggestionsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IDtoService _dtoService;
|
||||
@@ -55,8 +59,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] string? mediaType,
|
||||
[FromQuery] string? type,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool enableTotalRecordCount = false)
|
||||
@@ -67,8 +71,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
|
||||
MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
|
||||
MediaTypes = mediaType,
|
||||
IncludeItemTypes = type,
|
||||
IsVirtualItem = false,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
@@ -64,9 +64,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[HttpGet("Info")]
|
||||
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SystemInfo>> GetSystemInfo()
|
||||
public ActionResult<SystemInfo> GetSystemInfo()
|
||||
{
|
||||
return await _appHost.GetSystemInfo(CancellationToken.None).ConfigureAwait(false);
|
||||
return _appHost.GetSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,9 +76,9 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="PublicSystemInfo"/> with public info about the system.</returns>
|
||||
[HttpGet("Info/Public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PublicSystemInfo>> GetPublicSystemInfo()
|
||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||
{
|
||||
return await _appHost.GetPublicSystemInfo(CancellationToken.None).ConfigureAwait(false);
|
||||
return _appHost.GetPublicSystemInfo(Request.HttpContext.Connection.RemoteIpAddress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
@@ -41,8 +42,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param>
|
||||
/// <param name="isHd">Optional filter by items that are HD or not.</param>
|
||||
/// <param name="is4K">Optional filter by items that are 4K or not.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
|
||||
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param>
|
||||
/// <param name="isUnaired">Optional filter by items that are unaired episodes or not.</param>
|
||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||
@@ -55,41 +56,41 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
|
||||
/// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
|
||||
/// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</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="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
|
||||
/// <param name="searchTerm">Optional. Filter based on a search term.</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 delimeted. 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 based on item type. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</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 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="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</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="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="isPlayed">Optional filter by items that are played, or not.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param>
|
||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. 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 delimeted.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param>
|
||||
/// <param name="albumArtistIds">Optional. If specified, results will be filtered to include only those containing the specified album artist id.</param>
|
||||
/// <param name="contributingArtistIds">Optional. If specified, results will be filtered to include only those containing the specified contributing artist id.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
|
||||
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param>
|
||||
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
|
||||
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="isLocked">Optional filter by items that are locked.</param>
|
||||
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
|
||||
@@ -100,12 +101,12 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxWidth">Optional. Filter by the maximum width of the item.</param>
|
||||
/// <param name="maxHeight">Optional. Filter by the maximum height of the item.</param>
|
||||
/// <param name="is3D">Optional filter by items that are 3D, or not.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param>
|
||||
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param>
|
||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. 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="enableTotalRecordCount">Optional. Enable the total record count.</param>
|
||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
|
||||
@@ -124,8 +125,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery] string? locationTypes,
|
||||
[FromQuery] LocationType[] excludeLocationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
[FromQuery] double? minCommunityRating,
|
||||
@@ -138,41 +139,41 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] string? excludeItemIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? imageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? artists,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] string? artistIds,
|
||||
[FromQuery] string? albumArtistIds,
|
||||
[FromQuery] string? contributingArtistIds,
|
||||
[FromQuery] string? albums,
|
||||
[FromQuery] string? albumIds,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] string? videoTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
@@ -183,20 +184,19 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery] string? seriesStatus,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var includeItemTypes = "Trailer";
|
||||
var includeItemTypes = new[] { "Trailer" };
|
||||
|
||||
return _itemsController
|
||||
.GetItems(
|
||||
userId,
|
||||
userId,
|
||||
maxOfficialRating,
|
||||
hasThemeSong,
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -13,6 +14,7 @@ using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -57,7 +59,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="userId">The user id of the user to get the next up episodes for.</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="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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="seriesId">Optional. Filter by series 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="enableImges">Optional. Include image information in output.</param>
|
||||
@@ -72,17 +74,16 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? seriesId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] bool? enableImges,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
{
|
||||
var options = new DtoOptions()
|
||||
.AddItemFields(fields!)
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
|
||||
@@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="userId">The user id of the user to get the upcoming episodes for.</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="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="fields">Optional. Specify additional fields of information to return in the output.</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="enableImges">Optional. Include image information in output.</param>
|
||||
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||
@@ -130,11 +131,11 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] bool? enableImges,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
@@ -145,8 +146,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var parentIdGuid = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId);
|
||||
|
||||
var options = new DtoOptions()
|
||||
.AddItemFields(fields!)
|
||||
var options = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImges, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="seriesId">The series id.</param>
|
||||
/// <param name="userId">The user id.</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="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, TrailerUrls.</param>
|
||||
/// <param name="season">Optional filter by season number.</param>
|
||||
/// <param name="seasonId">Optional. Filter by season id.</param>
|
||||
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
|
||||
@@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="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="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>
|
||||
/// <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)]
|
||||
@@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetEpisodes(
|
||||
[FromRoute, Required] string seriesId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] int? season,
|
||||
[FromQuery] string? seasonId,
|
||||
[FromQuery] bool? isMissing,
|
||||
@@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] string? sortBy)
|
||||
{
|
||||
@@ -216,8 +216,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
List<BaseItem> episodes;
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields!)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
|
||||
@@ -304,7 +303,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </summary>
|
||||
/// <param name="seriesId">The series id.</param>
|
||||
/// <param name="userId">The user id.</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="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, TrailerUrls.</param>
|
||||
/// <param name="isSpecialSeason">Optional. Filter by special season.</param>
|
||||
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
|
||||
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>
|
||||
@@ -319,13 +318,13 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSeasons(
|
||||
[FromRoute, Required] string seriesId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? isSpecialSeason,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] string? adjacentTo,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
@@ -344,8 +343,7 @@ namespace Jellyfin.Api.Controllers
|
||||
AdjacentTo = adjacentTo
|
||||
});
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes!);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
@@ -76,6 +77,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxAudioChannels">Optional. The maximum number of audio channels.</param>
|
||||
/// <param name="transcodingAudioChannels">Optional. The number of how many audio channels to transcode to.</param>
|
||||
/// <param name="maxStreamingBitrate">Optional. The maximum streaming bitrate.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="transcodingContainer">Optional. The container to transcode to.</param>
|
||||
/// <param name="transcodingProtocol">Optional. The transcoding protocol.</param>
|
||||
@@ -88,23 +90,22 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="302">Redirected to remote audio stream.</response>
|
||||
/// <returns>A <see cref="Task"/> containing the audio file.</returns>
|
||||
[HttpGet("Audio/{itemId}/universal")]
|
||||
[HttpGet("Audio/{itemId}/universal.{container}", Name = "GetUniversalAudioStream_2")]
|
||||
[HttpHead("Audio/{itemId}/universal", Name = "HeadUniversalAudioStream")]
|
||||
[HttpHead("Audio/{itemId}/universal.{container}", Name = "HeadUniversalAudioStream_2")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetUniversalAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] int? transcodingAudioChannels,
|
||||
[FromQuery] long? maxStreamingBitrate,
|
||||
[FromQuery] int? maxStreamingBitrate,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] string? transcodingContainer,
|
||||
[FromQuery] string? transcodingProtocol,
|
||||
@@ -191,8 +192,11 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||
// ffmpeg option -> file extension
|
||||
// mpegts -> ts
|
||||
// fmp4 -> mp4
|
||||
// TODO: remove this when we switch back to the segment muxer
|
||||
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
|
||||
var supportedHlsContainers = new[] { "ts", "mp4" };
|
||||
|
||||
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||
{
|
||||
@@ -201,7 +205,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Static = isStatic,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
@@ -212,7 +216,7 @@ namespace Jellyfin.Api.Controllers
|
||||
AudioSampleRate = maxAudioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||
StartTimeTicks = startTimeTicks,
|
||||
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||
RequireAvc = true,
|
||||
@@ -244,7 +248,7 @@ namespace Jellyfin.Api.Controllers
|
||||
BreakOnNonKeyFrames = breakOnNonKeyFrames,
|
||||
AudioSampleRate = maxAudioSampleRate,
|
||||
MaxAudioChannels = maxAudioChannels,
|
||||
AudioBitRate = isStatic ? (int?)null : Convert.ToInt32(Math.Min(maxStreamingBitrate ?? 192000, int.MaxValue)),
|
||||
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
|
||||
MaxAudioBitDepth = maxAudioBitDepth,
|
||||
AudioChannels = maxAudioChannels,
|
||||
CopyTimestamps = true,
|
||||
@@ -258,7 +262,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
private DeviceProfile GetDeviceProfile(
|
||||
string? container,
|
||||
string[] containers,
|
||||
string? transcodingContainer,
|
||||
string? audioCodec,
|
||||
string? transcodingProtocol,
|
||||
@@ -270,20 +274,23 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
var deviceProfile = new DeviceProfile();
|
||||
|
||||
var directPlayProfiles = new List<DirectPlayProfile>();
|
||||
|
||||
var containers = RequestHelpers.Split(container, ',', true);
|
||||
|
||||
foreach (var cont in containers)
|
||||
int len = containers.Length;
|
||||
var directPlayProfiles = new DirectPlayProfile[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var parts = RequestHelpers.Split(cont, ',', true);
|
||||
var parts = RequestHelpers.Split(containers[i], '|', true);
|
||||
|
||||
var audioCodecs = parts.Length == 1 ? null : string.Join(",", parts.Skip(1).ToArray());
|
||||
var audioCodecs = parts.Length == 1 ? null : string.Join(',', parts.Skip(1));
|
||||
|
||||
directPlayProfiles.Add(new DirectPlayProfile { Type = DlnaProfileType.Audio, Container = parts[0], AudioCodec = audioCodecs });
|
||||
directPlayProfiles[i] = new DirectPlayProfile
|
||||
{
|
||||
Type = DlnaProfileType.Audio,
|
||||
Container = parts[0],
|
||||
AudioCodec = audioCodecs
|
||||
};
|
||||
}
|
||||
|
||||
deviceProfile.DirectPlayProfiles = directPlayProfiles.ToArray();
|
||||
deviceProfile.DirectPlayProfiles = directPlayProfiles;
|
||||
|
||||
deviceProfile.TranscodingProfiles = new[]
|
||||
{
|
||||
@@ -323,7 +330,7 @@ namespace Jellyfin.Api.Controllers
|
||||
if (conditions.Count > 0)
|
||||
{
|
||||
// codec profile
|
||||
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
|
||||
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
|
||||
}
|
||||
|
||||
deviceProfile.CodecProfiles = codecProfiles.ToArray();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@@ -381,17 +381,13 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
if (string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
|
||||
{
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
_userManager.UpdateConfiguration(user.Id, updateUser.Configuration);
|
||||
}
|
||||
else
|
||||
if (!string.Equals(user.Username, updateUser.Name, StringComparison.Ordinal))
|
||||
{
|
||||
await _userManager.RenameUser(user, updateUser.Name).ConfigureAwait(false);
|
||||
_userManager.UpdateConfiguration(updateUser.Id, updateUser.Configuration);
|
||||
}
|
||||
|
||||
await _userManager.UpdateConfigurationAsync(user.Id, updateUser.Configuration).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@@ -409,7 +405,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult UpdateUserPolicy(
|
||||
public async Task<ActionResult> UpdateUserPolicy(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UserPolicy newPolicy)
|
||||
{
|
||||
@@ -447,7 +443,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_sessionManager.RevokeUserTokens(user.Id, currentToken);
|
||||
}
|
||||
|
||||
_userManager.UpdatePolicy(userId, newPolicy);
|
||||
await _userManager.UpdatePolicyAsync(userId, newPolicy).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -464,7 +460,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult UpdateUserConfiguration(
|
||||
public async Task<ActionResult> UpdateUserConfiguration(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromBody] UserConfiguration userConfig)
|
||||
{
|
||||
@@ -473,7 +469,7 @@ namespace Jellyfin.Api.Controllers
|
||||
return Forbid("User configuration update not allowed");
|
||||
}
|
||||
|
||||
_userManager.UpdateConfiguration(userId, userConfig);
|
||||
await _userManager.UpdateConfigurationAsync(userId, userConfig).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
@@ -534,6 +530,33 @@ namespace Jellyfin.Api.Controllers
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user based on auth token.
|
||||
/// </summary>
|
||||
/// <response code="200">User returned.</response>
|
||||
/// <response code="400">Token is not owned by a user.</response>
|
||||
/// <returns>A <see cref="UserDto"/> for the authenticated user.</returns>
|
||||
[HttpGet("Me")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<UserDto> GetCurrentUser()
|
||||
{
|
||||
var userId = ClaimHelpers.GetUserId(Request.HttpContext.User);
|
||||
if (userId == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId.Value);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return _userManager.GetUserDto(user);
|
||||
}
|
||||
|
||||
private IEnumerable<UserDto> Get(bool? isHidden, bool? isDisabled, bool filterByDevice, bool filterByNetwork)
|
||||
{
|
||||
var users = _userManager.Users;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -251,8 +252,8 @@ namespace Jellyfin.Api.Controllers
|
||||
/// </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="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</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>
|
||||
@@ -267,12 +268,12 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<IEnumerable<BaseItemDto>> GetLatestMedia(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int limit = 20,
|
||||
[FromQuery] bool groupItems = true)
|
||||
@@ -287,8 +288,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
|
||||
new LatestItemsQuery
|
||||
{
|
||||
GroupItems = groupItems,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
IsPlayed = isPlayed,
|
||||
Limit = limit,
|
||||
ParentId = parentId ?? Guid.Empty,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.UserViewDtos;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] bool? includeExternalContent,
|
||||
[FromQuery] string? presetViews,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
|
||||
[FromQuery] bool includeHidden = false)
|
||||
{
|
||||
var query = new UserViewQuery
|
||||
@@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
|
||||
query.IncludeExternalContent = includeExternalContent.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(presetViews))
|
||||
if (presetViews.Length != 0)
|
||||
{
|
||||
query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
|
||||
query.PresetViews = presetViews;
|
||||
}
|
||||
|
||||
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -43,10 +44,10 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <response code="404">Video or attachment not found.</response>
|
||||
/// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
|
||||
[HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<FileStreamResult>> GetAttachment(
|
||||
public async Task<ActionResult> GetAttachment(
|
||||
[FromRoute, Required] Guid videoId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
[FromRoute, Required] int index)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@@ -11,6 +12,7 @@ using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.Models.PlaybackDtos;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
@@ -144,7 +146,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -295,23 +297,23 @@ namespace Jellyfin.Api.Controllers
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TranscodingJobDto? job = null;
|
||||
var playlist = state.OutputFilePath;
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
|
||||
|
||||
if (!System.IO.File.Exists(playlist))
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(playlist))
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
job = await _transcodingJobHelper.StartFfMpeg(
|
||||
state,
|
||||
playlist,
|
||||
GetCommandLineArguments(playlist, state),
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, state),
|
||||
Request,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource)
|
||||
@@ -327,7 +329,7 @@ namespace Jellyfin.Api.Controllers
|
||||
minSegments = state.MinSegments;
|
||||
if (minSegments > 0)
|
||||
{
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,14 +339,14 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
_transcodingJobHelper.OnTranscodeEndRequest(job);
|
||||
}
|
||||
|
||||
var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
|
||||
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
|
||||
|
||||
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
@@ -358,38 +360,67 @@ namespace Jellyfin.Api.Controllers
|
||||
private string GetCommandLineArguments(string outputPath, StreamState state)
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
|
||||
var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
|
||||
var outputTsArg = Path.Combine(Path.GetDirectoryName(outputPath), Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
|
||||
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
||||
|
||||
var segmentFormat = format.TrimStart('.');
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
|
||||
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
var outputTsArg = outputPrefix + "%d" + outputExtension;
|
||||
|
||||
var segmentFormat = outputExtension.TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
|
||||
}
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
|
||||
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
: "128";
|
||||
|
||||
var baseUrlParam = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"\"hls{0}\"",
|
||||
"\"hls/{0}/\"",
|
||||
Path.GetFileNameWithoutExtension(outputPath));
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"",
|
||||
inputModifier,
|
||||
_encodingHelper.GetInputArgument(state, _encodingOptions),
|
||||
threads,
|
||||
_encodingHelper.GetMapArgs(state),
|
||||
mapArgs,
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
maxMuxingQueueSize,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||
string.Empty,
|
||||
segmentFormat,
|
||||
baseUrlParam,
|
||||
outputPath,
|
||||
outputTsArg)
|
||||
.Trim();
|
||||
outputTsArg,
|
||||
outputPath).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -399,14 +430,53 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>The command line arguments for audio transcoding.</returns>
|
||||
private string GetAudioArguments(StreamState state)
|
||||
{
|
||||
var codec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
if (state.AudioStream == null)
|
||||
{
|
||||
return "-codec:a:0 copy";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioChannels.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
@@ -427,7 +497,7 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
@@ -439,6 +509,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>The command line arguments for video transcoding.</returns>
|
||||
private string GetVideoArguments(StreamState state)
|
||||
{
|
||||
if (state.VideoStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
return string.Empty;
|
||||
@@ -448,46 +523,64 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
// {
|
||||
// args += " -mpegts_m2ts_mode 1";
|
||||
// }
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
// See if we can save come cpu cycles by avoiding encoding.
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
{
|
||||
// if h264_mp4toannexb is ever added, do not use it for live tv
|
||||
if (state.VideoStream != null &&
|
||||
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
// If h264_mp4toannexb is ever added, do not use it for live tv.
|
||||
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
if (!string.IsNullOrEmpty(bitStreamArgs))
|
||||
{
|
||||
args += " " + bitStreamArgs;
|
||||
}
|
||||
}
|
||||
|
||||
args += " -start_at_zero";
|
||||
}
|
||||
else
|
||||
{
|
||||
var keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
|
||||
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
|
||||
|
||||
// Set the key frame params for video encoding to match the hls segment time.
|
||||
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
|
||||
|
||||
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
|
||||
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -bf 0";
|
||||
}
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// Graphical subs overlay and resolution params.
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resolution params.
|
||||
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
|
||||
{
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
args += " -start_at_zero";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
@@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds)
|
||||
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
|
||||
{
|
||||
var items = RequestHelpers.Split(itemIds, ',', true)
|
||||
var items = itemIds
|
||||
.Select(i => _libraryManager.GetItemById(i))
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.Id)
|
||||
@@ -283,7 +284,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
@@ -312,7 +313,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
@@ -326,15 +327,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
|
||||
[HttpGet("{itemId}/stream")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
@@ -529,5 +528,166 @@ namespace Jellyfin.Api.Controllers
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a video stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
return GetVideoStream(
|
||||
itemId,
|
||||
container,
|
||||
@static,
|
||||
@params,
|
||||
tag,
|
||||
deviceProfileId,
|
||||
playSessionId,
|
||||
segmentContainer,
|
||||
segmentLength,
|
||||
minSegments,
|
||||
mediaSourceId,
|
||||
deviceId,
|
||||
audioCodec,
|
||||
enableAutoStreamCopy,
|
||||
allowVideoStreamCopy,
|
||||
allowAudioStreamCopy,
|
||||
breakOnNonKeyFrames,
|
||||
audioSampleRate,
|
||||
maxAudioBitDepth,
|
||||
audioBitRate,
|
||||
audioChannels,
|
||||
maxAudioChannels,
|
||||
profile,
|
||||
level,
|
||||
framerate,
|
||||
maxFramerate,
|
||||
copyTimestamps,
|
||||
startTimeTicks,
|
||||
width,
|
||||
height,
|
||||
videoBitRate,
|
||||
subtitleStreamIndex,
|
||||
subtitleMethod,
|
||||
maxRefFrames,
|
||||
maxVideoBitDepth,
|
||||
requireAvc,
|
||||
deInterlace,
|
||||
requireNonAnamorphic,
|
||||
transcodingMaxAudioChannels,
|
||||
cpuCoreLimit,
|
||||
liveStreamId,
|
||||
enableMpegtsM2TsMode,
|
||||
videoCodec,
|
||||
subtitleCodec,
|
||||
transcodingReasons,
|
||||
audioStreamIndex,
|
||||
videoStreamIndex,
|
||||
context,
|
||||
streamOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -50,7 +52,7 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <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="fields">Optional. Specify additional fields of information to return in the output.</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>
|
||||
@@ -70,20 +72,19 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery] string? enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] bool recursive = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var dtoOptions = new DtoOptions()
|
||||
.AddItemFields(fields)
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
@@ -102,19 +103,15 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
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,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
|
||||
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||
|
||||
if (parentItem.IsFolder)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user