mirror of https://github.com/jellyfin/jellyfin.git
Merge branch 'master' into NetworkPR2
This commit is contained in:
commit
2c9e355e42
|
@ -0,0 +1,36 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
schedule:
|
||||||
|
- cron: '24 2 * * 4'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: [ 'csharp' ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup .NET Core
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: '5.0.100'
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
queries: +security-extended
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
|
@ -0,0 +1,130 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Events;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
using MediaBrowser.Model.Net;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Emby.Server.Implementations.Library
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A library post scan/refresh task for pre-fetching remote images.
|
||||||
|
/// </summary>
|
||||||
|
public class ImageFetcherPostScanTask : ILibraryPostScanTask
|
||||||
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly ILogger<ImageFetcherPostScanTask> _logger;
|
||||||
|
private readonly SemaphoreSlim _imageFetcherLock;
|
||||||
|
|
||||||
|
private ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)> _queuedItems;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ImageFetcherPostScanTask"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">An instance of <see cref="ILibraryManager"/>.</param>
|
||||||
|
/// <param name="providerManager">An instance of <see cref="IProviderManager"/>.</param>
|
||||||
|
/// <param name="logger">An instance of <see cref="ILogger{ImageFetcherPostScanTask}"/>.</param>
|
||||||
|
public ImageFetcherPostScanTask(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
ILogger<ImageFetcherPostScanTask> logger)
|
||||||
|
{
|
||||||
|
_libraryManager = libraryManager;
|
||||||
|
_providerManager = providerManager;
|
||||||
|
_logger = logger;
|
||||||
|
_queuedItems = new ConcurrentDictionary<Guid, (BaseItem item, ItemUpdateType updateReason)>();
|
||||||
|
_imageFetcherLock = new SemaphoreSlim(1, 1);
|
||||||
|
_libraryManager.ItemAdded += OnLibraryManagerItemAddedOrUpdated;
|
||||||
|
_libraryManager.ItemUpdated += OnLibraryManagerItemAddedOrUpdated;
|
||||||
|
_providerManager.RefreshCompleted += OnProviderManagerRefreshCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Sometimes a library scan will cause this to run twice if there's an item refresh going on.
|
||||||
|
await _imageFetcherLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var itemGuids = _queuedItems.Keys.ToList();
|
||||||
|
|
||||||
|
for (var i = 0; i < itemGuids.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_queuedItems.TryGetValue(itemGuids[i], out var queuedItem))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemId = queuedItem.item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||||
|
var itemType = queuedItem.item.GetType();
|
||||||
|
_logger.LogDebug(
|
||||||
|
"Updating remote images for item {ItemId} with media type {ItemMediaType}",
|
||||||
|
itemId,
|
||||||
|
itemType);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _libraryManager.UpdateImagesAsync(queuedItem.item, queuedItem.updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to fetch images for {Type} item with id {ItemId}", itemType, itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedItems.TryRemove(queuedItem.item.Id, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemGuids.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Finished updating/pre-fetching {NumberOfImages} images. Elapsed time: {TimeElapsed}s.",
|
||||||
|
itemGuids.Count.ToString(CultureInfo.InvariantCulture),
|
||||||
|
(DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No images were updated.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_imageFetcherLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLibraryManagerItemAddedOrUpdated(object sender, ItemChangeEventArgs itemChangeEventArgs)
|
||||||
|
{
|
||||||
|
if (!_queuedItems.ContainsKey(itemChangeEventArgs.Item.Id) && itemChangeEventArgs.Item.ImageInfos.Length > 0)
|
||||||
|
{
|
||||||
|
_queuedItems.AddOrUpdate(
|
||||||
|
itemChangeEventArgs.Item.Id,
|
||||||
|
(itemChangeEventArgs.Item, itemChangeEventArgs.UpdateReason),
|
||||||
|
(key, existingValue) => existingValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProviderManagerRefreshCompleted(object sender, GenericEventArgs<BaseItem> e)
|
||||||
|
{
|
||||||
|
if (!_queuedItems.ContainsKey(e.Argument.Id) && e.Argument.ImageInfos.Length > 0)
|
||||||
|
{
|
||||||
|
_queuedItems.AddOrUpdate(
|
||||||
|
e.Argument.Id,
|
||||||
|
(e.Argument, ItemUpdateType.None),
|
||||||
|
(key, existingValue) => existingValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The RefreshCompleted event is a bit awkward in that it seems to _only_ be fired on
|
||||||
|
// the item that was refreshed regardless of children refreshes. So we take it as a signal
|
||||||
|
// that the refresh is entirely completed.
|
||||||
|
Run(null, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -858,7 +858,21 @@ namespace Emby.Server.Implementations.Library
|
||||||
/// <returns>Task{Person}.</returns>
|
/// <returns>Task{Person}.</returns>
|
||||||
public Person GetPerson(string name)
|
public Person GetPerson(string name)
|
||||||
{
|
{
|
||||||
return CreateItemByName<Person>(Person.GetPath, name, new DtoOptions(true));
|
var path = Person.GetPath(name);
|
||||||
|
var id = GetItemByNameId<Person>(path);
|
||||||
|
if (!(GetItemById(id) is Person item))
|
||||||
|
{
|
||||||
|
item = new Person
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Id = id,
|
||||||
|
DateCreated = DateTime.UtcNow,
|
||||||
|
DateModified = DateTime.UtcNow,
|
||||||
|
Path = path
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1941,19 +1955,9 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
foreach (var item in items)
|
RunMetadataSavers(items, updateReason);
|
||||||
{
|
|
||||||
if (item.IsFileProtocol)
|
|
||||||
{
|
|
||||||
ProviderManager.SaveMetadata(item, updateReason);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.DateLastSaved = DateTime.UtcNow;
|
|
||||||
|
|
||||||
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_itemRepository.SaveItems(items, cancellationToken);
|
_itemRepository.SaveItems(items, cancellationToken);
|
||||||
|
|
||||||
|
@ -1984,12 +1988,27 @@ namespace Emby.Server.Implementations.Library
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||||
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
|
=> UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken);
|
||||||
|
|
||||||
|
public void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.IsFileProtocol)
|
||||||
|
{
|
||||||
|
ProviderManager.SaveMetadata(item, updateReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.DateLastSaved = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reports the item removed.
|
/// Reports the item removed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -111,12 +111,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort)))
|
using var client = new TcpClient();
|
||||||
using (var stream = client.GetStream())
|
client.Connect(remoteIp, HdHomeRunPort);
|
||||||
{
|
|
||||||
|
using var stream = client.GetStream();
|
||||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
|
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
@ -142,7 +142,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
{
|
{
|
||||||
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
|
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
|
||||||
|
|
||||||
_tcpClient = new TcpClient(_remoteEndPoint);
|
_tcpClient = new TcpClient();
|
||||||
|
_tcpClient.Connect(_remoteEndPoint);
|
||||||
|
|
||||||
if (!_lockkey.HasValue)
|
if (!_lockkey.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -221,9 +222,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var tcpClient = new TcpClient(_remoteEndPoint))
|
using var tcpClient = new TcpClient();
|
||||||
using (var stream = tcpClient.GetStream())
|
tcpClient.Connect(_remoteEndPoint);
|
||||||
{
|
|
||||||
|
using var stream = tcpClient.GetStream();
|
||||||
var commandList = commands.GetCommands();
|
var commandList = commands.GetCommands();
|
||||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||||
try
|
try
|
||||||
|
@ -246,7 +248,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopStreaming(TcpClient client)
|
public Task StopStreaming(TcpClient client)
|
||||||
{
|
{
|
||||||
|
|
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
|
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
|
||||||
localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
|
localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
|
||||||
tcpClient.Close();
|
tcpClient.Close();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
@ -103,6 +103,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (localAddress.IsIPv4MappedToIPv6) {
|
||||||
|
localAddress = localAddress.MapToIPv4();
|
||||||
|
}
|
||||||
|
|
||||||
var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork);
|
var udpClient = new UdpClient(localPort, AddressFamily.InterNetwork);
|
||||||
var hdHomerunManager = new HdHomerunManager();
|
var hdHomerunManager = new HdHomerunManager();
|
||||||
|
|
||||||
|
@ -133,12 +137,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
|
|
||||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||||
|
|
||||||
await StartStreaming(
|
_ = StartStreaming(
|
||||||
udpClient,
|
udpClient,
|
||||||
hdHomerunManager,
|
hdHomerunManager,
|
||||||
remoteAddress,
|
remoteAddress,
|
||||||
taskCompletionSource,
|
taskCompletionSource,
|
||||||
LiveStreamCancellationTokenSource.Token).ConfigureAwait(false);
|
LiveStreamCancellationTokenSource.Token);
|
||||||
|
|
||||||
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||||
// OpenedMediaSource.Path = tempFile;
|
// OpenedMediaSource.Path = tempFile;
|
||||||
|
@ -159,9 +163,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
return TempFilePath;
|
return TempFilePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
private async Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||||
{
|
|
||||||
return Task.Run(async () =>
|
|
||||||
{
|
{
|
||||||
using (udpClient)
|
using (udpClient)
|
||||||
using (hdHomerunManager)
|
using (hdHomerunManager)
|
||||||
|
@ -185,7 +187,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||||
}
|
}
|
||||||
|
|
||||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"Albums": "Albums",
|
"Albums": "Albums",
|
||||||
"Artists": "Các Nghệ Sĩ",
|
"Artists": "Các Nghệ Sĩ",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
|
"TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.",
|
||||||
"TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu",
|
"TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu",
|
||||||
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
|
"TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.",
|
||||||
"TaskRefreshChannels": "Làm Mới Kênh",
|
"TaskRefreshChannels": "Làm Mới Kênh",
|
||||||
"TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
|
"TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.",
|
||||||
|
@ -24,11 +24,11 @@
|
||||||
"TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
|
"TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.",
|
||||||
"TaskUpdatePlugins": "Cập Nhật Plugins",
|
"TaskUpdatePlugins": "Cập Nhật Plugins",
|
||||||
"TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
|
"TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.",
|
||||||
"TaskRefreshPeople": "Làm mới Người dùng",
|
"TaskRefreshPeople": "Làm Mới Người Dùng",
|
||||||
"TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
|
"TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.",
|
||||||
"TaskCleanLogs": "Làm sạch nhật ký",
|
"TaskCleanLogs": "Làm Sạch Thư Mục Nhật Ký",
|
||||||
"TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.",
|
"TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm tệp mới và làm mới dữ liệu mô tả.",
|
||||||
"TaskRefreshLibrary": "Quét Thư viện Phương tiện",
|
"TaskRefreshLibrary": "Quét Thư Viện Phương Tiện",
|
||||||
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
|
"TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.",
|
||||||
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
|
"TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh",
|
||||||
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
|
"TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.",
|
||||||
|
|
|
@ -93,17 +93,29 @@ namespace Emby.Server.Implementations.Updates
|
||||||
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default)
|
public async Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetAsync(manifest, cancellationToken).ConfigureAwait(false);
|
.GetAsync(new Uri(manifest), cancellationToken).ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false);
|
var package = await _jsonSerializer.DeserializeFromStreamAsync<IList<PackageInfo>>(stream).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Store the repository and repository url with each version, as they may be spread apart.
|
||||||
|
foreach (var entry in package)
|
||||||
|
{
|
||||||
|
foreach (var ver in entry.versions)
|
||||||
|
{
|
||||||
|
ver.repositoryName = manifestName;
|
||||||
|
ver.repositoryUrl = manifest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return package;
|
||||||
}
|
}
|
||||||
catch (SerializationException ex)
|
catch (SerializationException ex)
|
||||||
{
|
{
|
||||||
|
@ -123,19 +135,71 @@ namespace Emby.Server.Implementations.Updates
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void MergeSort(IList<VersionInfo> source, IList<VersionInfo> dest)
|
||||||
|
{
|
||||||
|
int sLength = source.Count - 1;
|
||||||
|
int dLength = dest.Count;
|
||||||
|
int s = 0, d = 0;
|
||||||
|
var sourceVersion = source[0].VersionNumber;
|
||||||
|
var destVersion = dest[0].VersionNumber;
|
||||||
|
|
||||||
|
while (d < dLength)
|
||||||
|
{
|
||||||
|
if (sourceVersion.CompareTo(destVersion) >= 0)
|
||||||
|
{
|
||||||
|
if (s < sLength)
|
||||||
|
{
|
||||||
|
sourceVersion = source[++s].VersionNumber;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Append all of destination to the end of source.
|
||||||
|
while (d < dLength)
|
||||||
|
{
|
||||||
|
source.Add(dest[d++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
source.Insert(s++, dest[d++]);
|
||||||
|
if (d >= dLength)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sLength++;
|
||||||
|
destVersion = dest[d].VersionNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var result = new List<PackageInfo>();
|
var result = new List<PackageInfo>();
|
||||||
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
|
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
|
||||||
{
|
{
|
||||||
foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true))
|
if (repository.Enabled)
|
||||||
|
{
|
||||||
|
// Where repositories have the same content, the details of the first is taken.
|
||||||
|
foreach (var package in await GetPackages(repository.Name, repository.Url, cancellationToken).ConfigureAwait(true))
|
||||||
|
{
|
||||||
|
var existing = FilterPackages(result, package.name, Guid.Parse(package.guid)).FirstOrDefault();
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Assumption is both lists are ordered, so slot these into the correct place.
|
||||||
|
MergeSort(existing.versions, package.versions);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
package.repositoryName = repository.Name;
|
|
||||||
package.repositoryUrl = repository.Url;
|
|
||||||
result.Add(package);
|
result.Add(package);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -144,7 +208,8 @@ namespace Emby.Server.Implementations.Updates
|
||||||
public IEnumerable<PackageInfo> FilterPackages(
|
public IEnumerable<PackageInfo> FilterPackages(
|
||||||
IEnumerable<PackageInfo> availablePackages,
|
IEnumerable<PackageInfo> availablePackages,
|
||||||
string name = null,
|
string name = null,
|
||||||
Guid guid = default)
|
Guid guid = default,
|
||||||
|
Version specificVersion = null)
|
||||||
{
|
{
|
||||||
if (name != null)
|
if (name != null)
|
||||||
{
|
{
|
||||||
|
@ -156,6 +221,11 @@ namespace Emby.Server.Implementations.Updates
|
||||||
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
|
availablePackages = availablePackages.Where(x => Guid.Parse(x.guid) == guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (specificVersion != null)
|
||||||
|
{
|
||||||
|
availablePackages = availablePackages.Where(x => x.versions.Where(y => y.VersionNumber.Equals(specificVersion)).Any());
|
||||||
|
}
|
||||||
|
|
||||||
return availablePackages;
|
return availablePackages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +237,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
Version minVersion = null,
|
Version minVersion = null,
|
||||||
Version specificVersion = null)
|
Version specificVersion = null)
|
||||||
{
|
{
|
||||||
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
|
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
|
||||||
|
|
||||||
// Package not found in repository
|
// Package not found in repository
|
||||||
if (package == null)
|
if (package == null)
|
||||||
|
@ -181,21 +251,21 @@ namespace Emby.Server.Implementations.Updates
|
||||||
|
|
||||||
if (specificVersion != null)
|
if (specificVersion != null)
|
||||||
{
|
{
|
||||||
availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion);
|
availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
|
||||||
}
|
}
|
||||||
else if (minVersion != null)
|
else if (minVersion != null)
|
||||||
{
|
{
|
||||||
availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion);
|
availableVersions = availableVersions.Where(x => x.VersionNumber >= minVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var v in availableVersions.OrderByDescending(x => x.version))
|
foreach (var v in availableVersions.OrderByDescending(x => x.VersionNumber))
|
||||||
{
|
{
|
||||||
yield return new InstallationInfo
|
yield return new InstallationInfo
|
||||||
{
|
{
|
||||||
Changelog = v.changelog,
|
Changelog = v.changelog,
|
||||||
Guid = new Guid(package.guid),
|
Guid = new Guid(package.guid),
|
||||||
Name = package.name,
|
Name = package.name,
|
||||||
Version = new Version(v.version),
|
Version = v.VersionNumber,
|
||||||
SourceUrl = v.sourceUrl,
|
SourceUrl = v.sourceUrl,
|
||||||
Checksum = v.checksum
|
Checksum = v.checksum
|
||||||
};
|
};
|
||||||
|
@ -333,7 +403,7 @@ namespace Emby.Server.Implementations.Updates
|
||||||
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
|
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
|
||||||
|
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||||
.GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false);
|
.GetAsync(new Uri(package.SourceUrl), cancellationToken).ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -85,15 +85,178 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
/// <response code="200">Audio stream returned.</response>
|
/// <response code="200">Audio stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
|
|
||||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||||
[HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
|
|
||||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesAudioFile]
|
[ProducesAudioFile]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
public async Task<ActionResult> GetAudioStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute] string? container,
|
[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>
|
||||||
|
/// <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 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">Audio stream returned.</response>
|
||||||
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
|
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
|
||||||
|
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesAudioFile]
|
||||||
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromRoute, Required] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Model.Branding;
|
using MediaBrowser.Model.Branding;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -838,7 +838,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute] string container,
|
[FromRoute, Required] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
|
@ -1009,7 +1009,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] string playlistId,
|
[FromRoute, Required] string playlistId,
|
||||||
[FromRoute, Required] int segmentId,
|
[FromRoute, Required] int segmentId,
|
||||||
[FromRoute] string container,
|
[FromRoute, Required] string container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
@ -86,7 +86,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="403">User does not have permission to delete the image.</response>
|
/// <response code="403">User does not have permission to delete the image.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
|
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
@ -95,7 +94,53 @@ namespace Jellyfin.Api.Controllers
|
||||||
public async Task<ActionResult> PostUserImage(
|
public async Task<ActionResult> PostUserImage(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute] int? index = null)
|
[FromQuery] int? index = null)
|
||||||
|
{
|
||||||
|
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)));
|
||||||
|
|
||||||
|
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))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
|
@ -132,8 +177,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="204">Image deleted.</response>
|
/// <response code="204">Image deleted.</response>
|
||||||
/// <response code="403">User does not have permission to delete the image.</response>
|
/// <response code="403">User does not have permission to delete the image.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpDelete("Users/{userId}/Images/{itemType}")]
|
[HttpDelete("Users/{userId}/Images/{imageType}")]
|
||||||
[HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
|
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
|
@ -142,7 +186,46 @@ namespace Jellyfin.Api.Controllers
|
||||||
public async Task<ActionResult> DeleteUserImage(
|
public async Task<ActionResult> DeleteUserImage(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute] int? index = null)
|
[FromQuery] int? index = null)
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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))
|
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||||
{
|
{
|
||||||
|
@ -173,14 +256,13 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="404">Item not found.</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>
|
/// <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}")]
|
||||||
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> DeleteItemImage(
|
public async Task<ActionResult> DeleteItemImage(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute] int? imageIndex = null)
|
[FromQuery] int? imageIndex)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -192,6 +274,65 @@ namespace Jellyfin.Api.Controllers
|
||||||
return NoContent();
|
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>
|
/// <summary>
|
||||||
/// Set item image.
|
/// Set item image.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -201,16 +342,15 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="204">Image saved.</response>
|
/// <response code="204">Image saved.</response>
|
||||||
/// <response code="404">Item not found.</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>
|
/// <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}")]
|
||||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[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] Guid itemId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute] int? imageIndex = null)
|
[FromRoute] int imageIndex)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -350,8 +490,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Items/{itemId}/Images/{imageType}")]
|
[HttpGet("Items/{itemId}/Images/{imageType}")]
|
||||||
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
|
[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.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -372,7 +510,86 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[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);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -508,8 +725,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
|
||||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
|
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -587,8 +804,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("Genres/{name}/Images/{imageType}")]
|
||||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
|
[HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -609,7 +826,86 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[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);
|
var item = _libraryManager.GetGenre(name);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -666,8 +962,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("MusicGenres/{name}/Images/{imageType}")]
|
||||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
|
[HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -688,7 +984,86 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[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);
|
var item = _libraryManager.GetMusicGenre(name);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -745,8 +1120,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("Persons/{name}/Images/{imageType}")]
|
||||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
|
[HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -767,7 +1142,86 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[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);
|
var item = _libraryManager.GetPerson(name);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -824,16 +1278,16 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("Studios/{name}/Images/{imageType}")]
|
||||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
|
[HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
public async Task<ActionResult> GetStudioImage(
|
public async Task<ActionResult> GetStudioImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromRoute, Required] ImageFormat format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
[FromQuery] double? percentPlayed,
|
[FromQuery] double? percentPlayed,
|
||||||
|
@ -846,7 +1300,86 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[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);
|
var item = _libraryManager.GetStudio(name);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
@ -903,8 +1436,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
[HttpGet("Users/{userId}/Images/{imageType}")]
|
||||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
|
[HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[ProducesImageFile]
|
[ProducesImageFile]
|
||||||
|
@ -925,7 +1458,104 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
[FromQuery] string? backgroundColor,
|
[FromQuery] string? backgroundColor,
|
||||||
[FromQuery] string? foregroundLayer,
|
[FromQuery] string? foregroundLayer,
|
||||||
[FromRoute] int? imageIndex = null)
|
[FromQuery] int? imageIndex)
|
||||||
|
{
|
||||||
|
var user = _userManager.GetUserById(userId);
|
||||||
|
if (user == 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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);
|
var user = _userManager.GetUserById(userId);
|
||||||
if (user?.ProfileImage == null)
|
if (user?.ProfileImage == null)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
/// <response code="200">Instant playlist returned.</response>
|
/// <response code="200">Instant playlist returned.</response>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
[HttpGet("Artists/InstantMix")]
|
[HttpGet("Artists/{id}/InstantMix")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||||
[FromRoute, Required] Guid id,
|
[FromRoute, Required] Guid id,
|
||||||
|
@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
/// <response code="200">Instant playlist returned.</response>
|
/// <response code="200">Instant playlist returned.</response>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
[HttpGet("MusicGenres/InstantMix")]
|
[HttpGet("MusicGenres/{id}/InstantMix")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
||||||
[FromRoute, Required] Guid id,
|
[FromRoute, Required] Guid id,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets items based on a query.
|
/// Gets items based on a query.
|
||||||
/// </summary>
|
/// </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="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="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="hasThemeSong">Optional filter by items with theme songs.</param>
|
||||||
|
@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||||
[HttpGet("Items")]
|
[HttpGet("Items")]
|
||||||
[HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||||
[FromRoute] Guid? uId,
|
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] string? maxOfficialRating,
|
[FromQuery] string? maxOfficialRating,
|
||||||
[FromQuery] bool? hasThemeSong,
|
[FromQuery] bool? hasThemeSong,
|
||||||
|
@ -228,9 +225,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] bool enableTotalRecordCount = true,
|
[FromQuery] bool enableTotalRecordCount = true,
|
||||||
[FromQuery] bool? enableImages = true)
|
[FromQuery] bool? enableImages = true)
|
||||||
{
|
{
|
||||||
// use user id route parameter over query parameter
|
|
||||||
userId = uId ?? userId;
|
|
||||||
|
|
||||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||||
? _userManager.GetUserById(userId.Value)
|
? _userManager.GetUserById(userId.Value)
|
||||||
: null;
|
: null;
|
||||||
|
@ -502,6 +496,257 @@ namespace Jellyfin.Api.Controllers
|
||||||
return new QueryResult<BaseItemDto> { StartIndex = startIndex.GetValueOrDefault(), TotalRecordCount = result.TotalRecordCount, Items = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user) };
|
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>
|
||||||
|
/// <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 delimeted.</param>
|
||||||
|
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</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 exxcluding item ids. This allows multiple, comma delimeted.</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="includeItemTypes">Optional. If specified, results will be filtered based on the 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="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="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="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="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="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="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 delimeted.</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="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("Users/{userId}/Items")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetItemsByUserId(
|
||||||
|
[FromRoute] 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)
|
||||||
|
{
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Gets items based on a query.
|
/// Gets items based on a query.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||||
if (!string.IsNullOrEmpty(repositoryUrl))
|
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();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
|
@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
public ActionResult<UserItemDataDto> MarkPlayedItem(
|
public ActionResult<UserItemDataDto> MarkPlayedItem(
|
||||||
[FromRoute, Required] Guid userId,
|
[FromRoute, Required] Guid userId,
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] DateTime? datePlayed)
|
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
using Jellyfin.Api.Models.SessionDtos;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Devices;
|
using MediaBrowser.Controller.Devices;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult PostFullCapabilities(
|
public ActionResult PostFullCapabilities(
|
||||||
[FromQuery] string? id,
|
[FromQuery] string? id,
|
||||||
[FromBody, Required] ClientCapabilities capabilities)
|
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(id))
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
{
|
{
|
||||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionManager.ReportCapabilities(id, capabilities);
|
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
|
|
|
@ -193,7 +193,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesFile("text/*")]
|
[ProducesFile("text/*")]
|
||||||
public async Task<ActionResult> GetSubtitle(
|
public async Task<ActionResult> GetSubtitle(
|
||||||
|
@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[FromQuery] long? endPositionTicks,
|
[FromQuery] long? endPositionTicks,
|
||||||
[FromQuery] bool copyTimestamps = false,
|
[FromQuery] bool copyTimestamps = false,
|
||||||
[FromQuery] bool addVttTimeMap = false,
|
[FromQuery] bool addVttTimeMap = false,
|
||||||
[FromRoute] long startPositionTicks = 0)
|
[FromQuery] long startPositionTicks = 0)
|
||||||
{
|
{
|
||||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
@ -249,6 +248,43 @@ namespace Jellyfin.Api.Controllers
|
||||||
MimeTypes.GetMimeType("file." + format));
|
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>
|
/// <summary>
|
||||||
/// Gets an HLS subtitle playlist.
|
/// Gets an HLS subtitle playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="204">Subtitle uploaded.</response>
|
/// <response code="204">Subtitle uploaded.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> UploadSubtitle(
|
public async Task<ActionResult> UploadSubtitle(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromBody, Required] UploadSubtitleDto body)
|
[FromBody, Required] UploadSubtitleDto body)
|
||||||
|
@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
[HttpGet("FallbackFont/Fonts/{name}")]
|
[HttpGet("FallbackFont/Fonts/{name}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesFile("font/*")]
|
||||||
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
||||||
{
|
{
|
||||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using MediaBrowser.Model.SyncPlay;
|
using MediaBrowser.Model.SyncPlay;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
|
@ -197,7 +197,6 @@ namespace Jellyfin.Api.Controllers
|
||||||
|
|
||||||
return _itemsController
|
return _itemsController
|
||||||
.GetItems(
|
.GetItems(
|
||||||
userId,
|
|
||||||
userId,
|
userId,
|
||||||
maxOfficialRating,
|
maxOfficialRating,
|
||||||
hasThemeSong,
|
hasThemeSong,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Attributes;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
|
@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <response code="404">Video or attachment not found.</response>
|
/// <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>
|
/// <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}")]
|
[HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
|
||||||
[Produces(MediaTypeNames.Application.Octet)]
|
[ProducesFile(MediaTypeNames.Application.Octet)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> GetAttachment(
|
public async Task<ActionResult> GetAttachment(
|
||||||
|
|
|
@ -327,15 +327,13 @@ namespace Jellyfin.Api.Controllers
|
||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
/// <response code="200">Video stream returned.</response>
|
/// <response code="200">Video stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
|
|
||||||
[HttpGet("{itemId}/stream")]
|
[HttpGet("{itemId}/stream")]
|
||||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
|
|
||||||
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public async Task<ActionResult> GetVideoStream(
|
public async Task<ActionResult> GetVideoStream(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute] string? container,
|
[FromQuery] string? container,
|
||||||
[FromQuery] bool? @static,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? @params,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
|
@ -530,5 +528,166 @@ namespace Jellyfin.Api.Controllers
|
||||||
_transcodingJobType,
|
_transcodingJobType,
|
||||||
cancellationTokenSource).ConfigureAwait(false);
|
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,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.ModelBinders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// DateTime model binder.
|
||||||
|
/// </summary>
|
||||||
|
public class LegacyDateTimeModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
// Borrowed from the DateTimeModelBinderProvider
|
||||||
|
private const DateTimeStyles SupportedStyles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AllowWhiteSpaces;
|
||||||
|
private readonly DateTimeModelBinder _defaultModelBinder;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LegacyDateTimeModelBinder"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
|
public LegacyDateTimeModelBinder(ILoggerFactory loggerFactory)
|
||||||
|
{
|
||||||
|
_defaultModelBinder = new DateTimeModelBinder(SupportedStyles, loggerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
|
if (valueProviderResult.Values.Count == 1)
|
||||||
|
{
|
||||||
|
var dateTimeString = valueProviderResult.FirstValue;
|
||||||
|
// Mark Played Item.
|
||||||
|
if (DateTime.TryParseExact(dateTimeString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
|
||||||
|
{
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(dateTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _defaultModelBinder.BindModelAsync(bindingContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.ModelBinders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Nullable enum model binder.
|
||||||
|
/// </summary>
|
||||||
|
public class NullableEnumModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
private readonly ILogger<NullableEnumModelBinder> _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="NullableEnumModelBinder"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{NullableEnumModelBinder}"/> interface.</param>
|
||||||
|
public NullableEnumModelBinder(ILogger<NullableEnumModelBinder> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
|
||||||
|
var elementType = bindingContext.ModelType.GetElementType() ?? bindingContext.ModelType.GenericTypeArguments[0];
|
||||||
|
var converter = TypeDescriptor.GetConverter(elementType);
|
||||||
|
if (valueProviderResult.Length != 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var convertedValue = converter.ConvertFromString(valueProviderResult.FirstValue);
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(convertedValue);
|
||||||
|
}
|
||||||
|
catch (FormatException e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Error converting value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.ModelBinders
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Nullable enum model binder provider.
|
||||||
|
/// </summary>
|
||||||
|
public class NullableEnumModelBinderProvider : IModelBinderProvider
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IModelBinder? GetBinder(ModelBinderProviderContext context)
|
||||||
|
{
|
||||||
|
var nullableType = Nullable.GetUnderlyingType(context.Metadata.ModelType);
|
||||||
|
if (nullableType == null || !nullableType.IsEnum)
|
||||||
|
{
|
||||||
|
// Type isn't nullable or isn't an enum.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = context.Services.GetRequiredService<ILogger<NullableEnumModelBinder>>();
|
||||||
|
return new NullableEnumModelBinder(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using MediaBrowser.Common.Json.Converters;
|
||||||
|
using MediaBrowser.Model.Dlna;
|
||||||
|
using MediaBrowser.Model.Session;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Models.SessionDtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client capabilities dto.
|
||||||
|
/// </summary>
|
||||||
|
public class ClientCapabilitiesDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of playable media types.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<string> PlayableMediaTypes { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of supported commands.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||||
|
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; } = Array.Empty<GeneralCommandType>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether session supports media control.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsMediaControl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether session supports content uploading.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsContentUploading { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the message callback url.
|
||||||
|
/// </summary>
|
||||||
|
public string? MessageCallbackUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether session supports a persistent identifier.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsPersistentIdentifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether session supports sync.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsSync { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the device profile.
|
||||||
|
/// </summary>
|
||||||
|
public DeviceProfile? DeviceProfile { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the app store url.
|
||||||
|
/// </summary>
|
||||||
|
public string? AppStoreUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the icon url.
|
||||||
|
/// </summary>
|
||||||
|
public string? IconUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert the dto to the full <see cref="ClientCapabilities"/> model.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The converted <see cref="ClientCapabilities"/> model.</returns>
|
||||||
|
public ClientCapabilities ToClientCapabilities()
|
||||||
|
{
|
||||||
|
return new ClientCapabilities
|
||||||
|
{
|
||||||
|
PlayableMediaTypes = PlayableMediaTypes,
|
||||||
|
SupportedCommands = SupportedCommands,
|
||||||
|
SupportsMediaControl = SupportsMediaControl,
|
||||||
|
SupportsContentUploading = SupportsContentUploading,
|
||||||
|
MessageCallbackUrl = MessageCallbackUrl,
|
||||||
|
SupportsPersistentIdentifier = SupportsPersistentIdentifier,
|
||||||
|
SupportsSync = SupportsSync,
|
||||||
|
DeviceProfile = DeviceProfile,
|
||||||
|
AppStoreUrl = AppStoreUrl,
|
||||||
|
IconUrl = IconUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.TypeConverters
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Custom datetime parser.
|
|
||||||
/// </summary>
|
|
||||||
public class DateTimeTypeConverter : TypeConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
|
||||||
{
|
|
||||||
if (sourceType == typeof(string))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.CanConvertFrom(context, sourceType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
|
||||||
{
|
|
||||||
if (value is string dateString)
|
|
||||||
{
|
|
||||||
// Mark Played Item.
|
|
||||||
if (DateTime.TryParseExact(dateString, "yyyyMMddHHmmss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTime))
|
|
||||||
{
|
|
||||||
return dateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Activity Logs.
|
|
||||||
if (DateTime.TryParse(dateString, null, DateTimeStyles.RoundtripKind, out dateTime))
|
|
||||||
{
|
|
||||||
return dateTime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return base.ConvertFrom(context, culture, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,7 @@ using Jellyfin.Api.Auth.LocalAccessPolicy;
|
||||||
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
using Jellyfin.Api.Auth.RequiresElevationPolicy;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Controllers;
|
using Jellyfin.Api.Controllers;
|
||||||
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Server.Configuration;
|
using Jellyfin.Server.Configuration;
|
||||||
using Jellyfin.Server.Filters;
|
using Jellyfin.Server.Filters;
|
||||||
using Jellyfin.Server.Formatters;
|
using Jellyfin.Server.Formatters;
|
||||||
|
@ -169,6 +170,8 @@ namespace Jellyfin.Server.Extensions
|
||||||
|
|
||||||
opts.OutputFormatters.Add(new CssOutputFormatter());
|
opts.OutputFormatters.Add(new CssOutputFormatter());
|
||||||
opts.OutputFormatters.Add(new XmlOutputFormatter());
|
opts.OutputFormatters.Add(new XmlOutputFormatter());
|
||||||
|
|
||||||
|
opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear app parts to avoid other assemblies being picked up
|
// Clear app parts to avoid other assemblies being picked up
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
using MediaBrowser.Model.Updates;
|
using MediaBrowser.Model.Updates;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Jellyfin.Api.TypeConverters;
|
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
using Jellyfin.Server.Extensions;
|
using Jellyfin.Server.Extensions;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
|
@ -67,10 +64,16 @@ namespace Jellyfin.Server
|
||||||
var productHeader = new ProductInfoHeaderValue(
|
var productHeader = new ProductInfoHeaderValue(
|
||||||
_serverApplicationHost.Name.Replace(' ', '-'),
|
_serverApplicationHost.Name.Replace(' ', '-'),
|
||||||
_serverApplicationHost.ApplicationVersionString);
|
_serverApplicationHost.ApplicationVersionString);
|
||||||
|
var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0);
|
||||||
|
var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9);
|
||||||
|
var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8);
|
||||||
services
|
services
|
||||||
.AddHttpClient(NamedClient.Default, c =>
|
.AddHttpClient(NamedClient.Default, c =>
|
||||||
{
|
{
|
||||||
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
||||||
|
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
|
||||||
|
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
||||||
|
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
||||||
})
|
})
|
||||||
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
||||||
|
|
||||||
|
@ -78,6 +81,8 @@ namespace Jellyfin.Server
|
||||||
{
|
{
|
||||||
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
||||||
c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
|
c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
|
||||||
|
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
|
||||||
|
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
||||||
})
|
})
|
||||||
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
||||||
|
|
||||||
|
@ -165,9 +170,6 @@ namespace Jellyfin.Server
|
||||||
endpoints.MapHealthChecks("/health");
|
endpoints.MapHealthChecks("/health");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add type descriptor for legacy datetime parsing.
|
|
||||||
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,10 +19,11 @@ namespace MediaBrowser.Common.Updates
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses a plugin manifest at the supplied URL.
|
/// Parses a plugin manifest at the supplied URL.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="manifestName">Name of the repository.</param>
|
||||||
/// <param name="manifest">The URL to query.</param>
|
/// <param name="manifest">The URL to query.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
|
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
|
||||||
Task<IReadOnlyList<PackageInfo>> GetPackages(string manifest, CancellationToken cancellationToken = default);
|
Task<IList<PackageInfo>> GetPackages(string manifestName, string manifest, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets all available packages.
|
/// Gets all available packages.
|
||||||
|
@ -37,11 +38,13 @@ namespace MediaBrowser.Common.Updates
|
||||||
/// <param name="availablePackages">The available packages.</param>
|
/// <param name="availablePackages">The available packages.</param>
|
||||||
/// <param name="name">The name of the plugin.</param>
|
/// <param name="name">The name of the plugin.</param>
|
||||||
/// <param name="guid">The id of the plugin.</param>
|
/// <param name="guid">The id of the plugin.</param>
|
||||||
|
/// <param name="specificVersion">The version of the plugin.</param>
|
||||||
/// <returns>All plugins matching the requirements.</returns>
|
/// <returns>All plugins matching the requirements.</returns>
|
||||||
IEnumerable<PackageInfo> FilterPackages(
|
IEnumerable<PackageInfo> FilterPackages(
|
||||||
IEnumerable<PackageInfo> availablePackages,
|
IEnumerable<PackageInfo> availablePackages,
|
||||||
string name = null,
|
string name = null,
|
||||||
Guid guid = default);
|
Guid guid = default,
|
||||||
|
Version specificVersion = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns all compatible versions ordered from newest to oldest.
|
/// Returns all compatible versions ordered from newest to oldest.
|
||||||
|
|
|
@ -1385,7 +1385,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
new List<FileSystemMetadata>();
|
new List<FileSystemMetadata>();
|
||||||
|
|
||||||
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
|
var ownedItemsChanged = await RefreshedOwnedItems(options, files, cancellationToken).ConfigureAwait(false);
|
||||||
await LibraryManager.UpdateImagesAsync(this).ConfigureAwait(false); // ensure all image properties in DB are fresh
|
|
||||||
|
|
||||||
if (ownedItemsChanged)
|
if (ownedItemsChanged)
|
||||||
{
|
{
|
||||||
|
|
|
@ -353,11 +353,6 @@ namespace MediaBrowser.Controller.Entities
|
||||||
{
|
{
|
||||||
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
await currentChild.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
// metadata is up-to-date; make sure DB has correct images dimensions and hash
|
|
||||||
await LibraryManager.UpdateImagesAsync(currentChild).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -571,6 +571,8 @@ namespace MediaBrowser.Controller.Library
|
||||||
string videoPath,
|
string videoPath,
|
||||||
string[] files);
|
string[] files);
|
||||||
|
|
||||||
|
void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason);
|
||||||
|
|
||||||
BaseItem GetParentItem(string parentId, Guid? userId);
|
BaseItem GetParentItem(string parentId, Guid? userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
@ -54,7 +55,7 @@ namespace MediaBrowser.Controller.Session
|
||||||
/// Gets or sets the playable media types.
|
/// Gets or sets the playable media types.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The playable media types.</value>
|
/// <value>The playable media types.</value>
|
||||||
public string[] PlayableMediaTypes
|
public IReadOnlyList<string> PlayableMediaTypes
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
@ -230,7 +231,7 @@ namespace MediaBrowser.Controller.Session
|
||||||
/// Gets or sets the supported commands.
|
/// Gets or sets the supported commands.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The supported commands.</value>
|
/// <value>The supported commands.</value>
|
||||||
public GeneralCommandType[] SupportedCommands
|
public IReadOnlyList<GeneralCommandType> SupportedCommands
|
||||||
=> Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
|
=> Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
|
||||||
|
|
||||||
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
|
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
|
||||||
|
|
|
@ -2,15 +2,16 @@
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
namespace MediaBrowser.Model.Session
|
namespace MediaBrowser.Model.Session
|
||||||
{
|
{
|
||||||
public class ClientCapabilities
|
public class ClientCapabilities
|
||||||
{
|
{
|
||||||
public string[] PlayableMediaTypes { get; set; }
|
public IReadOnlyList<string> PlayableMediaTypes { get; set; }
|
||||||
|
|
||||||
public GeneralCommandType[] SupportedCommands { get; set; }
|
public IReadOnlyList<GeneralCommandType> SupportedCommands { get; set; }
|
||||||
|
|
||||||
public bool SupportsMediaControl { get; set; }
|
public bool SupportsMediaControl { get; set; }
|
||||||
|
|
||||||
|
|
|
@ -50,17 +50,7 @@ namespace MediaBrowser.Model.Updates
|
||||||
/// Gets or sets the versions.
|
/// Gets or sets the versions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The versions.</value>
|
/// <value>The versions.</value>
|
||||||
public IReadOnlyList<VersionInfo> versions { get; set; }
|
public IList<VersionInfo> versions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the repository name.
|
|
||||||
/// </summary>
|
|
||||||
public string repositoryName { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the repository url.
|
|
||||||
/// </summary>
|
|
||||||
public string repositoryUrl { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
|
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
|
||||||
|
|
|
@ -16,5 +16,11 @@ namespace MediaBrowser.Model.Updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The URL.</value>
|
/// <value>The URL.</value>
|
||||||
public string? Url { get; set; }
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether the repository is enabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <value><c>true</c> if enabled.</value>
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,11 +9,29 @@ namespace MediaBrowser.Model.Updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class VersionInfo
|
public class VersionInfo
|
||||||
{
|
{
|
||||||
|
private Version _version;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the version.
|
/// Gets or sets the version.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The version.</value>
|
/// <value>The version.</value>
|
||||||
public string version { get; set; }
|
public string version
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _version == null ? string.Empty : _version.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_version = Version.Parse(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the version as a <see cref="Version"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Version VersionNumber => _version;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the changelog for this version.
|
/// Gets or sets the changelog for this version.
|
||||||
|
@ -44,5 +62,15 @@ namespace MediaBrowser.Model.Updates
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The timestamp.</value>
|
/// <value>The timestamp.</value>
|
||||||
public string timestamp { get; set; }
|
public string timestamp { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the repository name.
|
||||||
|
/// </summary>
|
||||||
|
public string repositoryName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the repository url.
|
||||||
|
/// </summary>
|
||||||
|
public string repositoryUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,16 +229,16 @@ namespace MediaBrowser.Providers.Manager
|
||||||
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
|
private Task SavePeopleMetadataAsync(List<PersonInfo> people, LibraryOptions libraryOptions, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
var personsToSave = new List<BaseItem>();
|
||||||
|
|
||||||
foreach (var person in people)
|
foreach (var person in people)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
||||||
{
|
{
|
||||||
var updateType = ItemUpdateType.MetadataDownload;
|
|
||||||
|
|
||||||
var saveEntity = false;
|
var saveEntity = false;
|
||||||
var personEntity = LibraryManager.GetPerson(person.Name);
|
var personEntity = LibraryManager.GetPerson(person.Name);
|
||||||
foreach (var id in person.ProviderIds)
|
foreach (var id in person.ProviderIds)
|
||||||
|
@ -261,15 +261,18 @@ namespace MediaBrowser.Providers.Manager
|
||||||
0);
|
0);
|
||||||
|
|
||||||
saveEntity = true;
|
saveEntity = true;
|
||||||
updateType |= ItemUpdateType.ImageUpdate;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saveEntity)
|
if (saveEntity)
|
||||||
{
|
{
|
||||||
await personEntity.UpdateToRepositoryAsync(updateType, cancellationToken).ConfigureAwait(false);
|
personsToSave.Add(personEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LibraryManager.RunMetadataSavers(personsToSave, ItemUpdateType.MetadataDownload);
|
||||||
|
LibraryManager.CreateItems(personsToSave, null, CancellationToken.None);
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
protected virtual Task AfterMetadataRefresh(TItemType item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||||
|
|
|
@ -47,6 +47,7 @@ namespace Jellyfin.Naming.Tests.Video
|
||||||
// FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
|
// FIXME: [InlineData("Robin Hood [Multi-Subs] [2018].mkv", "Robin Hood", 2018)]
|
||||||
[InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
|
[InlineData(@"3.Days.to.Kill.2014.720p.BluRay.x264.YIFY.mkv", "3.Days.to.Kill", 2014)] // In this test case, running CleanDateTime first produces no date, so it will attempt to run CleanString first and then CleanDateTime again
|
||||||
[InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)]
|
[InlineData("3 days to kill (2005).mkv", "3 days to kill", 2005)]
|
||||||
|
[InlineData(@"Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4", "Rain Man", 1988)]
|
||||||
[InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)]
|
[InlineData("My Movie 2013.12.09", "My Movie 2013.12.09", null)]
|
||||||
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
|
[InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)]
|
||||||
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
|
[InlineData("My Movie 20131209", "My Movie 20131209", null)]
|
||||||
|
|
|
@ -145,6 +145,14 @@ namespace Jellyfin.Naming.Tests.Video
|
||||||
name: "Brave",
|
name: "Brave",
|
||||||
year: 2006)
|
year: 2006)
|
||||||
};
|
};
|
||||||
|
yield return new object[]
|
||||||
|
{
|
||||||
|
new VideoFileInfo(
|
||||||
|
path: @"/server/Movies/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem/Rain Man 1988 REMASTERED 1080p BluRay x264 AAC - Ozlem.mp4",
|
||||||
|
container: "mp4",
|
||||||
|
name: "Rain Man",
|
||||||
|
year: 1988)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
Loading…
Reference in New Issue