Merge branch 'master' into NetworkPR2
This commit is contained in:
commit
2c9e355e42
36
.github/workflows/codeql-analysis.yml
vendored
Normal file
36
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -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
|
130
Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs
Normal file
130
Emby.Server.Implementations/Library/ImageFetcherPostScanTask.cs
Normal file
|
@ -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>
|
||||
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>
|
||||
|
@ -1941,19 +1955,9 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (item.IsFileProtocol)
|
||||
{
|
||||
ProviderManager.SaveMetadata(item, updateReason);
|
||||
}
|
||||
|
||||
item.DateLastSaved = DateTime.UtcNow;
|
||||
|
||||
await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false);
|
||||
}
|
||||
RunMetadataSavers(items, updateReason);
|
||||
|
||||
_itemRepository.SaveItems(items, cancellationToken);
|
||||
|
||||
|
@ -1984,12 +1988,27 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken 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>
|
||||
/// Reports the item removed.
|
||||
/// </summary>
|
||||
|
|
|
@ -111,11 +111,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort)))
|
||||
using (var stream = client.GetStream())
|
||||
{
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
using var client = new TcpClient();
|
||||
client.Connect(remoteIp, HdHomeRunPort);
|
||||
|
||||
using var stream = client.GetStream();
|
||||
return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
_tcpClient = new TcpClient(_remoteEndPoint);
|
||||
_tcpClient = new TcpClient();
|
||||
_tcpClient.Connect(_remoteEndPoint);
|
||||
|
||||
if (!_lockkey.HasValue)
|
||||
{
|
||||
|
@ -221,30 +222,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
return;
|
||||
}
|
||||
|
||||
using (var tcpClient = new TcpClient(_remoteEndPoint))
|
||||
using (var stream = tcpClient.GetStream())
|
||||
{
|
||||
var commandList = commands.GetCommands();
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
try
|
||||
{
|
||||
foreach (var command in commandList)
|
||||
{
|
||||
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
|
||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
using var tcpClient = new TcpClient();
|
||||
tcpClient.Connect(_remoteEndPoint);
|
||||
|
||||
// parse response to make sure it worked
|
||||
if (!ParseReturnMessage(buffer, receivedBytes, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
using var stream = tcpClient.GetStream();
|
||||
var commandList = commands.GetCommands();
|
||||
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
|
||||
try
|
||||
{
|
||||
foreach (var command in commandList)
|
||||
{
|
||||
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
|
||||
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
|
||||
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// parse response to make sure it worked
|
||||
if (!ParseReturnMessage(buffer, receivedBytes, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
try
|
||||
{
|
||||
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
|
||||
localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address;
|
||||
localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
|
||||
tcpClient.Close();
|
||||
}
|
||||
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 hdHomerunManager = new HdHomerunManager();
|
||||
|
||||
|
@ -133,12 +137,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
|
||||
var taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
|
||||
await StartStreaming(
|
||||
_ = StartStreaming(
|
||||
udpClient,
|
||||
hdHomerunManager,
|
||||
remoteAddress,
|
||||
taskCompletionSource,
|
||||
LiveStreamCancellationTokenSource.Token).ConfigureAwait(false);
|
||||
LiveStreamCancellationTokenSource.Token);
|
||||
|
||||
// OpenedMediaSource.Protocol = MediaProtocol.File;
|
||||
// OpenedMediaSource.Path = tempFile;
|
||||
|
@ -159,33 +163,30 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
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 (hdHomerunManager)
|
||||
{
|
||||
using (udpClient)
|
||||
using (hdHomerunManager)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error opening live stream:");
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
EnableStreamSharing = false;
|
||||
await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error opening live stream:");
|
||||
openTaskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
|
||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||
});
|
||||
EnableStreamSharing = false;
|
||||
}
|
||||
|
||||
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task CopyTo(UdpClient udpClient, string file, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"Albums": "Albums",
|
||||
"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ả.",
|
||||
"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.",
|
||||
"TaskRefreshChannels": "Làm Mới Kênh",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"TaskCleanLogs": "Làm sạch 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.",
|
||||
"TaskRefreshLibrary": "Quét Thư viện Phương tiện",
|
||||
"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 tệp mới và làm mới dữ liệu mô tả.",
|
||||
"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.",
|
||||
"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.",
|
||||
|
|
|
@ -93,17 +93,29 @@ namespace Emby.Server.Implementations.Updates
|
|||
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
|
||||
|
||||
/// <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
|
||||
{
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -123,17 +135,69 @@ 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 />
|
||||
public async Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = new List<PackageInfo>();
|
||||
foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories)
|
||||
{
|
||||
foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true))
|
||||
if (repository.Enabled)
|
||||
{
|
||||
package.repositoryName = repository.Name;
|
||||
package.repositoryUrl = repository.Url;
|
||||
result.Add(package);
|
||||
// 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
|
||||
{
|
||||
result.Add(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -144,7 +208,8 @@ namespace Emby.Server.Implementations.Updates
|
|||
public IEnumerable<PackageInfo> FilterPackages(
|
||||
IEnumerable<PackageInfo> availablePackages,
|
||||
string name = null,
|
||||
Guid guid = default)
|
||||
Guid guid = default,
|
||||
Version specificVersion = null)
|
||||
{
|
||||
if (name != null)
|
||||
{
|
||||
|
@ -156,6 +221,11 @@ namespace Emby.Server.Implementations.Updates
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -167,7 +237,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
Version minVersion = null,
|
||||
Version specificVersion = null)
|
||||
{
|
||||
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
|
||||
var package = FilterPackages(availablePackages, name, guid, specificVersion).FirstOrDefault();
|
||||
|
||||
// Package not found in repository
|
||||
if (package == null)
|
||||
|
@ -181,21 +251,21 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
if (specificVersion != null)
|
||||
{
|
||||
availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion);
|
||||
availableVersions = availableVersions.Where(x => x.VersionNumber.Equals(specificVersion));
|
||||
}
|
||||
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
|
||||
{
|
||||
Changelog = v.changelog,
|
||||
Guid = new Guid(package.guid),
|
||||
Name = package.name,
|
||||
Version = new Version(v.version),
|
||||
Version = v.VersionNumber,
|
||||
SourceUrl = v.sourceUrl,
|
||||
Checksum = v.checksum
|
||||
};
|
||||
|
@ -333,7 +403,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name);
|
||||
|
||||
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);
|
||||
|
||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -85,15 +85,178 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Audio stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/stream.{container:required}", Name = "GetAudioStreamByContainer")]
|
||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||
[HttpHead("{itemId}/stream.{container:required}", Name = "HeadAudioStreamByContainer")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetAudioStream(
|
||||
[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] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Branding;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
|
@ -838,7 +838,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute] string container,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
|
@ -1009,7 +1009,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromRoute, Required] int segmentId,
|
||||
[FromRoute] string container,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||
[HttpPost("Users/{userId}/Images/{imageType}/{index?}", Name = "PostUserImage_2")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
|
@ -95,7 +94,53 @@ namespace Jellyfin.Api.Controllers
|
|||
public async Task<ActionResult> PostUserImage(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? index = null)
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
@ -132,8 +177,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="204">Image deleted.</response>
|
||||
/// <response code="403">User does not have permission to delete the image.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Users/{userId}/Images/{itemType}")]
|
||||
[HttpDelete("Users/{userId}/Images/{itemType}/{index?}", Name = "DeleteUserImage_2")]
|
||||
[HttpDelete("Users/{userId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
|
@ -142,7 +186,46 @@ namespace Jellyfin.Api.Controllers
|
|||
public async Task<ActionResult> DeleteUserImage(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? index = null)
|
||||
[FromQuery] int? index = null)
|
||||
{
|
||||
if (!RequestHelpers.AssertCanUpdateUser(_authContext, HttpContext.Request, userId, true))
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
@ -173,14 +256,13 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "DeleteItemImage_2")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteItemImage(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
|
@ -192,6 +274,65 @@ namespace Jellyfin.Api.Controllers
|
|||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an item's image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">The image index.</param>
|
||||
/// <response code="204">Image deleted.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpDelete("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await item.DeleteImageAsync(imageType, imageIndex).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set item image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <response code="204">Image saved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> SetItemImage(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, Request.Body, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set item image.
|
||||
/// </summary>
|
||||
|
@ -201,16 +342,15 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="204">Image saved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "SetItemImage_2")]
|
||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||
public async Task<ActionResult> SetItemImage(
|
||||
public async Task<ActionResult> SetItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromRoute] int imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
|
@ -350,8 +490,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// </returns>
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}", Name = "HeadItemImage")]
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "GetItemImage_2")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex?}", Name = "HeadItemImage_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -372,7 +510,86 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
itemId,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item's image.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Items/{itemId}/Images/{imageType}/{imageIndex}", Name = "HeadItemImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetItemImageByIndex(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute] int imageIndex,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
|
@ -508,8 +725,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadArtistImage")]
|
||||
[HttpGet("Artists/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Artists/{name}/Images/{imageType}/{imageIndex}", Name = "HeadArtistImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -587,8 +804,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadGenreImage")]
|
||||
[HttpGet("Genres/{name}/Images/{imageType}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}", Name = "HeadGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -609,7 +826,86 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get genre image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Genre name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Genres/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Genres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadGenreImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetGenreImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetGenre(name);
|
||||
if (item == null)
|
||||
|
@ -666,8 +962,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadMusicGenreImage")]
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}", Name = "HeadMusicGenreImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -688,7 +984,86 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get music genre image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Music genre name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("MusicGenres/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("MusicGenres/{name}/Images/{imageType}/{imageIndex}", Name = "HeadMusicGenreImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetMusicGenreImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetMusicGenre(name);
|
||||
if (item == null)
|
||||
|
@ -745,8 +1120,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadPersonImage")]
|
||||
[HttpGet("Persons/{name}/Images/{imageType}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}", Name = "HeadPersonImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -767,7 +1142,86 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get person image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Person name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Persons/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Persons/{name}/Images/{imageType}/{imageIndex}", Name = "HeadPersonImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetPersonImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(name);
|
||||
if (item == null)
|
||||
|
@ -824,16 +1278,16 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex?}", Name = "HeadStudioImage")]
|
||||
[HttpGet("Studios/{name}/Images/{imageType}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}", Name = "HeadStudioImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetStudioImage(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] string tag,
|
||||
[FromRoute, Required] ImageFormat format,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
|
@ -846,7 +1300,86 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
if (item == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
item.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
item,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get studio image by name.
|
||||
/// </summary>
|
||||
/// <param name="name">Studio name.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Studios/{name}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Studios/{name}/Images/{imageType}/{imageIndex}", Name = "HeadStudioImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetStudioImageByIndex(
|
||||
[FromRoute, Required] string name,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var item = _libraryManager.GetStudio(name);
|
||||
if (item == null)
|
||||
|
@ -903,8 +1436,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex?}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex?}", Name = "HeadUserImage")]
|
||||
[HttpGet("Users/{userId}/Images/{imageType}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}", Name = "HeadUserImage")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
|
@ -925,7 +1458,104 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
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);
|
||||
if (user?.ProfileImage == null)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
@ -206,7 +206,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Artists/InstantMix")]
|
||||
[HttpGet("Artists/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||
[FromRoute, Required] Guid id,
|
||||
|
@ -242,7 +242,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("MusicGenres/InstantMix")]
|
||||
[HttpGet("MusicGenres/{id}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
||||
[FromRoute, Required] Guid id,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
|
|
@ -60,7 +60,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <summary>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
/// <param name="uId">The user id supplied in the /Users/{uid}/Items.</param>
|
||||
/// <param name="userId">The user id supplied as query parameter.</param>
|
||||
/// <param name="maxOfficialRating">Optional filter by maximum official rating (PG, PG-13, TV-MA, etc).</param>
|
||||
/// <param name="hasThemeSong">Optional filter by items with theme songs.</param>
|
||||
|
@ -143,10 +142,8 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
|
||||
[HttpGet("Items")]
|
||||
[HttpGet("Users/{uId}/Items", Name = "GetItems_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetItems(
|
||||
[FromRoute] Guid? uId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? maxOfficialRating,
|
||||
[FromQuery] bool? hasThemeSong,
|
||||
|
@ -228,9 +225,6 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
// use user id route parameter over query parameter
|
||||
userId = uId ?? userId;
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
@ -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) };
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Gets items based on a query.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -99,7 +99,7 @@ namespace Jellyfin.Api.Controllers
|
|||
var packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(repositoryUrl))
|
||||
{
|
||||
packages = packages.Where(p => p.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase))
|
||||
packages = packages.Where(p => p.versions.Where(q => q.repositoryUrl.Equals(repositoryUrl, StringComparison.OrdinalIgnoreCase)).Any())
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
|
@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
|||
public ActionResult<UserItemDataDto> MarkPlayedItem(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] DateTime? datePlayed)
|
||||
[FromQuery, ModelBinder(typeof(LegacyDateTimeModelBinder))] DateTime? datePlayed)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
var session = RequestHelpers.GetSession(_sessionManager, _authContext, Request);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Threading;
|
|||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.SessionDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
|
|||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostFullCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromBody, Required] ClientCapabilities capabilities)
|
||||
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
}
|
||||
|
||||
_sessionManager.ReportCapabilities(id, capabilities);
|
||||
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
|
|
|
@ -193,7 +193,6 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks?}/Stream.{format}", Name = "GetSubtitle_2")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetSubtitle(
|
||||
|
@ -204,7 +203,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false,
|
||||
[FromRoute] long startPositionTicks = 0)
|
||||
[FromQuery] long startPositionTicks = 0)
|
||||
{
|
||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -249,6 +248,43 @@ namespace Jellyfin.Api.Controllers
|
|||
MimeTypes.GetMimeType("file." + format));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets subtitles in a specified format.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="mediaSourceId">The media source id.</param>
|
||||
/// <param name="index">The subtitle stream index.</param>
|
||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
||||
/// <param name="format">The format of the returned subtitle.</param>
|
||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("text/*")]
|
||||
public Task<ActionResult> GetSubtitleWithTicks(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string mediaSourceId,
|
||||
[FromRoute, Required] int index,
|
||||
[FromRoute, Required] long startPositionTicks,
|
||||
[FromRoute, Required] string format,
|
||||
[FromQuery] long? endPositionTicks,
|
||||
[FromQuery] bool copyTimestamps = false,
|
||||
[FromQuery] bool addVttTimeMap = false)
|
||||
{
|
||||
return GetSubtitle(
|
||||
itemId,
|
||||
mediaSourceId,
|
||||
index,
|
||||
format,
|
||||
endPositionTicks,
|
||||
copyTimestamps,
|
||||
addVttTimeMap,
|
||||
startPositionTicks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an HLS subtitle playlist.
|
||||
/// </summary>
|
||||
|
@ -335,6 +371,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="204">Subtitle uploaded.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> UploadSubtitle(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromBody, Required] UploadSubtitleDto body)
|
||||
|
@ -446,6 +483,7 @@ namespace Jellyfin.Api.Controllers
|
|||
[HttpGet("FallbackFont/Fonts/{name}")]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesFile("font/*")]
|
||||
public ActionResult GetFallbackFont([FromRoute, Required] string name)
|
||||
{
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
|
@ -197,7 +197,6 @@ namespace Jellyfin.Api.Controllers
|
|||
|
||||
return _itemsController
|
||||
.GetItems(
|
||||
userId,
|
||||
userId,
|
||||
maxOfficialRating,
|
||||
hasThemeSong,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
|
@ -43,7 +44,7 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <response code="404">Video or attachment not found.</response>
|
||||
/// <returns>An <see cref="FileStreamResult"/> containing the attachment stream on success, or a <see cref="NotFoundResult"/> if the attachment could not be found.</returns>
|
||||
[HttpGet("{videoId}/{mediaSourceId}/Attachments/{index}")]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile(MediaTypeNames.Application.Octet)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> GetAttachment(
|
||||
|
|
|
@ -327,15 +327,13 @@ namespace Jellyfin.Api.Controllers
|
|||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
|
||||
[HttpGet("{itemId}/stream")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
|
@ -530,5 +528,166 @@ namespace Jellyfin.Api.Controllers
|
|||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a video stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
return GetVideoStream(
|
||||
itemId,
|
||||
container,
|
||||
@static,
|
||||
@params,
|
||||
tag,
|
||||
deviceProfileId,
|
||||
playSessionId,
|
||||
segmentContainer,
|
||||
segmentLength,
|
||||
minSegments,
|
||||
mediaSourceId,
|
||||
deviceId,
|
||||
audioCodec,
|
||||
enableAutoStreamCopy,
|
||||
allowVideoStreamCopy,
|
||||
allowAudioStreamCopy,
|
||||
breakOnNonKeyFrames,
|
||||
audioSampleRate,
|
||||
maxAudioBitDepth,
|
||||
audioBitRate,
|
||||
audioChannels,
|
||||
maxAudioChannels,
|
||||
profile,
|
||||
level,
|
||||
framerate,
|
||||
maxFramerate,
|
||||
copyTimestamps,
|
||||
startTimeTicks,
|
||||
width,
|
||||
height,
|
||||
videoBitRate,
|
||||
subtitleStreamIndex,
|
||||
subtitleMethod,
|
||||
maxRefFrames,
|
||||
maxVideoBitDepth,
|
||||
requireAvc,
|
||||
deInterlace,
|
||||
requireNonAnamorphic,
|
||||
transcodingMaxAudioChannels,
|
||||
cpuCoreLimit,
|
||||
liveStreamId,
|
||||
enableMpegtsM2TsMode,
|
||||
videoCodec,
|
||||
subtitleCodec,
|
||||
transcodingReasons,
|
||||
audioStreamIndex,
|
||||
videoStreamIndex,
|
||||
context,
|
||||
streamOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
|
49
Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs
Normal file
49
Jellyfin.Api/ModelBinders/LegacyDateTimeModelBinder.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
47
Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
Normal file
47
Jellyfin.Api/ModelBinders/NullableEnumModelBinder.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
27
Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs
Normal file
27
Jellyfin.Api/ModelBinders/NullableEnumModelBinderProvider.cs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
87
Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
Normal file
87
Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs
Normal file
|
@ -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.Constants;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Server.Configuration;
|
||||
using Jellyfin.Server.Filters;
|
||||
using Jellyfin.Server.Formatters;
|
||||
|
@ -169,6 +170,8 @@ namespace Jellyfin.Server.Extensions
|
|||
|
||||
opts.OutputFormatters.Add(new CssOutputFormatter());
|
||||
opts.OutputFormatters.Add(new XmlOutputFormatter());
|
||||
|
||||
opts.ModelBinderProviders.Insert(0, new NullableEnumModelBinderProvider());
|
||||
})
|
||||
|
||||
// Clear app parts to avoid other assemblies being picked up
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Updates;
|
||||
|
||||
|
@ -46,4 +46,4 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using Jellyfin.Api.TypeConverters;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.Implementations;
|
||||
|
@ -67,10 +64,16 @@ namespace Jellyfin.Server
|
|||
var productHeader = new ProductInfoHeaderValue(
|
||||
_serverApplicationHost.Name.Replace(' ', '-'),
|
||||
_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
|
||||
.AddHttpClient(NamedClient.Default, c =>
|
||||
{
|
||||
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
||||
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
|
||||
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
||||
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
||||
|
||||
|
@ -78,6 +81,8 @@ namespace Jellyfin.Server
|
|||
{
|
||||
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
|
||||
c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})"));
|
||||
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
|
||||
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
||||
})
|
||||
.ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler());
|
||||
|
||||
|
@ -165,9 +170,6 @@ namespace Jellyfin.Server
|
|||
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>
|
||||
/// Parses a plugin manifest at the supplied URL.
|
||||
/// </summary>
|
||||
/// <param name="manifestName">Name of the repository.</param>
|
||||
/// <param name="manifest">The URL to query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <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>
|
||||
/// Gets all available packages.
|
||||
|
@ -37,11 +38,13 @@ namespace MediaBrowser.Common.Updates
|
|||
/// <param name="availablePackages">The available packages.</param>
|
||||
/// <param name="name">The name 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>
|
||||
IEnumerable<PackageInfo> FilterPackages(
|
||||
IEnumerable<PackageInfo> availablePackages,
|
||||
string name = null,
|
||||
Guid guid = default);
|
||||
Guid guid = default,
|
||||
Version specificVersion = null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all compatible versions ordered from newest to oldest.
|
||||
|
|
|
@ -1385,7 +1385,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
new List<FileSystemMetadata>();
|
||||
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -353,11 +353,6 @@ namespace MediaBrowser.Controller.Entities
|
|||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -571,6 +571,8 @@ namespace MediaBrowser.Controller.Library
|
|||
string videoPath,
|
||||
string[] files);
|
||||
|
||||
void RunMetadataSavers(IReadOnlyList<BaseItem> items, ItemUpdateType updateReason);
|
||||
|
||||
BaseItem GetParentItem(string parentId, Guid? userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
@ -54,7 +55,7 @@ namespace MediaBrowser.Controller.Session
|
|||
/// Gets or sets the playable media types.
|
||||
/// </summary>
|
||||
/// <value>The playable media types.</value>
|
||||
public string[] PlayableMediaTypes
|
||||
public IReadOnlyList<string> PlayableMediaTypes
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -230,7 +231,7 @@ namespace MediaBrowser.Controller.Session
|
|||
/// Gets or sets the supported commands.
|
||||
/// </summary>
|
||||
/// <value>The supported commands.</value>
|
||||
public GeneralCommandType[] SupportedCommands
|
||||
public IReadOnlyList<GeneralCommandType> SupportedCommands
|
||||
=> Capabilities == null ? Array.Empty<GeneralCommandType>() : Capabilities.SupportedCommands;
|
||||
|
||||
public Tuple<ISessionController, bool> EnsureController<T>(Func<SessionInfo, ISessionController> factory)
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Model.Session
|
||||
{
|
||||
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; }
|
||||
|
||||
|
|
|
@ -50,17 +50,7 @@ namespace MediaBrowser.Model.Updates
|
|||
/// Gets or sets the versions.
|
||||
/// </summary>
|
||||
/// <value>The versions.</value>
|
||||
public IReadOnlyList<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; }
|
||||
public IList<VersionInfo> versions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PackageInfo"/> class.
|
||||
|
|
|
@ -16,5 +16,11 @@ namespace MediaBrowser.Model.Updates
|
|||
/// </summary>
|
||||
/// <value>The URL.</value>
|
||||
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>
|
||||
public class VersionInfo
|
||||
{
|
||||
private Version _version;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the version.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// Gets or sets the changelog for this version.
|
||||
|
@ -44,5 +62,15 @@ namespace MediaBrowser.Model.Updates
|
|||
/// </summary>
|
||||
/// <value>The timestamp.</value>
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (person.ProviderIds.Count > 0 || !string.IsNullOrWhiteSpace(person.ImageUrl))
|
||||
{
|
||||
var updateType = ItemUpdateType.MetadataDownload;
|
||||
|
||||
var saveEntity = false;
|
||||
var personEntity = LibraryManager.GetPerson(person.Name);
|
||||
foreach (var id in person.ProviderIds)
|
||||
|
@ -261,15 +261,18 @@ namespace MediaBrowser.Providers.Manager
|
|||
0);
|
||||
|
||||
saveEntity = true;
|
||||
updateType |= ItemUpdateType.ImageUpdate;
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -47,6 +47,7 @@ namespace Jellyfin.Naming.Tests.Video
|
|||
// 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 (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 20131209", "My Movie 20131209", null)]
|
||||
|
|
|
@ -145,6 +145,14 @@ namespace Jellyfin.Naming.Tests.Video
|
|||
name: "Brave",
|
||||
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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user