mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-11-16 02:18:54 -07:00
Merge branch 'master' into library_scan_speed
This commit is contained in:
commit
e8cb9cea7d
@ -30,11 +30,11 @@ jobs:
|
||||
|
||||
# This is required for the SonarCloud analyzer
|
||||
- task: UseDotNet@2
|
||||
displayName: "Install .NET Core SDK 2.1"
|
||||
displayName: "Install .NET SDK 5.x"
|
||||
condition: eq(variables['ImageName'], 'ubuntu-latest')
|
||||
inputs:
|
||||
packageType: sdk
|
||||
version: '2.1.805'
|
||||
version: '5.x'
|
||||
|
||||
- task: UseDotNet@2
|
||||
displayName: "Update DotNet"
|
||||
|
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
|
@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
{
|
||||
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
|
||||
|
||||
if (query.ChannelIds.Length > 0)
|
||||
if (query.ChannelIds.Count > 0)
|
||||
{
|
||||
// Avoid implicitly captured closure
|
||||
var ids = query.ChannelIds;
|
||||
|
@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data
|
||||
whereClauses.Add($"type in ({inClause})");
|
||||
}
|
||||
|
||||
if (query.ChannelIds.Length == 1)
|
||||
if (query.ChannelIds.Count == 1)
|
||||
{
|
||||
whereClauses.Add("ChannelId=@ChannelId");
|
||||
statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
else if (query.ChannelIds.Length > 1)
|
||||
else if (query.ChannelIds.Count > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
|
||||
whereClauses.Add($"ChannelId in ({inClause})");
|
||||
@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data
|
||||
whereClauses.Add(clause);
|
||||
}
|
||||
|
||||
if (query.GenreIds.Length > 0)
|
||||
if (query.GenreIds.Count > 0)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
var index = 0;
|
||||
@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data
|
||||
whereClauses.Add(clause);
|
||||
}
|
||||
|
||||
if (query.Genres.Length > 0)
|
||||
if (query.Genres.Count > 0)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
var index = 0;
|
||||
|
@ -1,61 +1,38 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly IJsonSerializer _json;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly object _capabilitiesSyncLock = new object();
|
||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
||||
public DeviceManager(
|
||||
IAuthenticationRepository authRepo,
|
||||
IJsonSerializer json,
|
||||
IUserManager userManager,
|
||||
IServerConfigurationManager config,
|
||||
IMemoryCache memoryCache)
|
||||
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
|
||||
{
|
||||
_json = json;
|
||||
_userManager = userManager;
|
||||
_config = config;
|
||||
_memoryCache = memoryCache;
|
||||
_authRepo = authRepo;
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
{
|
||||
var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
_memoryCache.Set(deviceId, capabilities);
|
||||
_json.SerializeToFile(capabilities, path);
|
||||
}
|
||||
_capabilitiesMap[deviceId] = capabilities;
|
||||
}
|
||||
|
||||
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
|
||||
@ -72,32 +49,12 @@ namespace Emby.Server.Implementations.Devices
|
||||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
lock (_capabilitiesSyncLock)
|
||||
{
|
||||
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
|
||||
try
|
||||
{
|
||||
return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return new ClientCapabilities();
|
||||
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
|
||||
? result
|
||||
: new ClientCapabilities();
|
||||
}
|
||||
|
||||
public DeviceInfo GetDevice(string id)
|
||||
{
|
||||
return GetDevice(id, true);
|
||||
}
|
||||
|
||||
private DeviceInfo GetDevice(string id, bool includeCapabilities)
|
||||
{
|
||||
var session = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
@ -154,16 +111,6 @@ namespace Emby.Server.Implementations.Devices
|
||||
};
|
||||
}
|
||||
|
||||
private string GetDevicesPath()
|
||||
{
|
||||
return Path.Combine(_config.ApplicationPaths.DataPath, "devices");
|
||||
}
|
||||
|
||||
private string GetDevicePath(string id)
|
||||
{
|
||||
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public bool CanAccessDevice(User user, string deviceId)
|
||||
{
|
||||
if (user == null)
|
||||
|
@ -1503,7 +1503,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (query.AncestorIds.Length == 0 &&
|
||||
query.ParentId.Equals(Guid.Empty) &&
|
||||
query.ChannelIds.Length == 0 &&
|
||||
query.ChannelIds.Count == 0 &&
|
||||
query.TopParentIds.Length == 0 &&
|
||||
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
|
||||
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,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)
|
||||
@ -80,6 +80,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();
|
||||
|
||||
@ -110,12 +114,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;
|
||||
@ -136,33 +140,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)
|
||||
|
@ -113,5 +113,10 @@
|
||||
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
|
||||
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
|
||||
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
|
||||
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας."
|
||||
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
|
||||
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.",
|
||||
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
|
||||
"Undefined": "Απροσδιόριστο",
|
||||
"Forced": "Εξαναγκασμένο",
|
||||
"Default": "Προεπιλογή"
|
||||
}
|
||||
|
@ -115,5 +115,8 @@
|
||||
"TaskRefreshChannels": "Csatornák frissítése",
|
||||
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
|
||||
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
|
||||
"TaskCleanActivityLog": "Tevékenységnapló törlése"
|
||||
"TaskCleanActivityLog": "Tevékenységnapló törlése",
|
||||
"Undefined": "Meghatározatlan",
|
||||
"Forced": "Kényszerített",
|
||||
"Default": "Alapértelmezett"
|
||||
}
|
||||
|
@ -18,13 +18,12 @@ namespace Emby.Server.Implementations.Networking
|
||||
public class NetworkManager : INetworkManager
|
||||
{
|
||||
private readonly ILogger<NetworkManager> _logger;
|
||||
|
||||
private IPAddress[] _localIpAddresses;
|
||||
private readonly object _localIpAddressSyncLock = new object();
|
||||
|
||||
private readonly object _subnetLookupLock = new object();
|
||||
private readonly Dictionary<string, List<string>> _subnetLookup = new Dictionary<string, List<string>>(StringComparer.Ordinal);
|
||||
|
||||
private IPAddress[] _localIpAddresses;
|
||||
|
||||
private List<PhysicalAddress> _macAddresses;
|
||||
|
||||
/// <summary>
|
||||
@ -157,7 +156,9 @@ namespace Emby.Server.Implementations.Networking
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] octet = ipAddress.GetAddressBytes();
|
||||
// GetAddressBytes
|
||||
Span<byte> octet = stackalloc byte[ipAddress.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
|
||||
ipAddress.TryWriteBytes(octet, out _);
|
||||
|
||||
if ((octet[0] == 10) ||
|
||||
(octet[0] == 172 && (octet[1] >= 16 && octet[1] <= 31)) || // RFC1918
|
||||
@ -260,7 +261,9 @@ namespace Emby.Server.Implementations.Networking
|
||||
/// <inheritdoc/>
|
||||
public bool IsAddressInSubnets(IPAddress address, bool excludeInterfaces, bool excludeRFC)
|
||||
{
|
||||
byte[] octet = address.GetAddressBytes();
|
||||
// GetAddressBytes
|
||||
Span<byte> octet = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
|
||||
address.TryWriteBytes(octet, out _);
|
||||
|
||||
if ((octet[0] == 127) || // RFC1122
|
||||
(octet[0] == 169 && octet[1] == 254)) // RFC3927
|
||||
@ -503,18 +506,25 @@ namespace Emby.Server.Implementations.Networking
|
||||
|
||||
private IPAddress GetNetworkAddress(IPAddress address, IPAddress subnetMask)
|
||||
{
|
||||
byte[] ipAdressBytes = address.GetAddressBytes();
|
||||
byte[] subnetMaskBytes = subnetMask.GetAddressBytes();
|
||||
int size = address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16;
|
||||
|
||||
if (ipAdressBytes.Length != subnetMaskBytes.Length)
|
||||
// GetAddressBytes
|
||||
Span<byte> ipAddressBytes = stackalloc byte[size];
|
||||
address.TryWriteBytes(ipAddressBytes, out _);
|
||||
|
||||
// GetAddressBytes
|
||||
Span<byte> subnetMaskBytes = stackalloc byte[size];
|
||||
subnetMask.TryWriteBytes(subnetMaskBytes, out _);
|
||||
|
||||
if (ipAddressBytes.Length != subnetMaskBytes.Length)
|
||||
{
|
||||
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
|
||||
}
|
||||
|
||||
byte[] broadcastAddress = new byte[ipAdressBytes.Length];
|
||||
byte[] broadcastAddress = new byte[ipAddressBytes.Length];
|
||||
for (int i = 0; i < broadcastAddress.Length; i++)
|
||||
{
|
||||
broadcastAddress[i] = (byte)(ipAdressBytes[i] & subnetMaskBytes[i]);
|
||||
broadcastAddress[i] = (byte)(ipAddressBytes[i] & subnetMaskBytes[i]);
|
||||
}
|
||||
|
||||
return new IPAddress(broadcastAddress);
|
||||
|
@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (options.ItemIdList.Length > 0)
|
||||
if (options.ItemIdList.Count > 0)
|
||||
{
|
||||
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
|
||||
{
|
||||
@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
|
||||
}
|
||||
|
||||
public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId)
|
||||
public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
|
||||
{
|
||||
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
|
||||
|
||||
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
});
|
||||
}
|
||||
|
||||
private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
|
||||
private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
|
||||
{
|
||||
// Retrieve the existing playlist
|
||||
var playlist = _libraryManager.GetItemById(playlistId) as Playlist
|
||||
|
@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <summary>
|
||||
/// The active connections.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections =
|
||||
new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private Timer _idleTimer;
|
||||
|
||||
@ -196,7 +195,7 @@ namespace Emby.Server.Implementations.Session
|
||||
{
|
||||
if (!string.IsNullOrEmpty(info.DeviceId))
|
||||
{
|
||||
var capabilities = GetSavedCapabilities(info.DeviceId);
|
||||
var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
|
||||
|
||||
if (capabilities != null)
|
||||
{
|
||||
@ -1677,27 +1676,10 @@ namespace Emby.Server.Implementations.Session
|
||||
SessionInfo = session
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
SaveCapabilities(session.DeviceId, capabilities);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error saving device capabilities", ex);
|
||||
}
|
||||
_deviceManager.SaveCapabilities(session.DeviceId, capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
private ClientCapabilities GetSavedCapabilities(string deviceId)
|
||||
{
|
||||
return _deviceManager.GetCapabilities(deviceId);
|
||||
}
|
||||
|
||||
private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
{
|
||||
_deviceManager.SaveCapabilities(deviceId, capabilities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a BaseItem to a BaseItemInfo.
|
||||
/// </summary>
|
||||
|
@ -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;
|
||||
@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
@ -174,9 +170,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers
|
||||
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId);
|
||||
}
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
NameLessThan = nameLessThan,
|
||||
NameStartsWith = nameStartsWith,
|
||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
@ -382,9 +374,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var (baseItem, itemCounts) = i;
|
||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(includeItemTypes))
|
||||
if (includeItemTypes.Length != 0)
|
||||
{
|
||||
dto.ChildCount = itemCounts.ItemCount;
|
||||
dto.ProgramCount = itemCounts.ProgramCount;
|
||||
|
@ -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;
|
||||
|
@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? channelIds)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
|
||||
{
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
Limit = limit,
|
||||
StartIndex = startIndex,
|
||||
ChannelIds = (channelIds ?? string.Empty)
|
||||
.Split(',')
|
||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||
.Select(i => new Guid(i))
|
||||
.ToArray(),
|
||||
ChannelIds = channelIds,
|
||||
DtoOptions = new DtoOptions { Fields = fields }
|
||||
};
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery] bool isLocked = false)
|
||||
{
|
||||
@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsLocked = isLocked,
|
||||
Name = name,
|
||||
ParentId = parentId,
|
||||
ItemIdList = RequestHelpers.Split(ids, ',', true),
|
||||
ItemIdList = ids,
|
||||
UserIds = new[] { userId }
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpPost("{collectionId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddToCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
|
||||
public async Task<ActionResult> AddToCollection(
|
||||
[FromRoute, Required] Guid collectionId,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
{
|
||||
await _collectionManager.AddToCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(true);
|
||||
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||
[HttpDelete("{collectionId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> RemoveFromCollection([FromRoute, Required] Guid collectionId, [FromQuery, Required] string ids)
|
||||
public async Task<ActionResult> RemoveFromCollection(
|
||||
[FromRoute, Required] Guid collectionId,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||
{
|
||||
await _collectionManager.RemoveFromCollectionAsync(collectionId, RequestHelpers.GetGuids(ids)).ConfigureAwait(false);
|
||||
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public class DynamicHlsController : BaseJellyfinApiController
|
||||
{
|
||||
private const string DefaultEncoderPreset = "veryfast";
|
||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
|
||||
private readonly ILogger<DynamicHlsController> _logger;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly DynamicHlsHelper _dynamicHlsHelper;
|
||||
|
||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
|
||||
private readonly EncodingOptions _encodingOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
|
||||
@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
|
||||
ILogger<DynamicHlsController> logger,
|
||||
DynamicHlsHelper dynamicHlsHelper)
|
||||
{
|
||||
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
|
||||
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
_dlnaManager = dlnaManager;
|
||||
@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_logger = logger;
|
||||
_dynamicHlsHelper = dynamicHlsHelper;
|
||||
|
||||
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -272,7 +276,7 @@ namespace Jellyfin.Api.Controllers
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -439,7 +443,7 @@ namespace Jellyfin.Api.Controllers
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
};
|
||||
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -834,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,
|
||||
@ -1005,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,
|
||||
@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@ -1137,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var segmentLengths = GetSegmentLengths(state);
|
||||
|
||||
var segmentContainer = state.Request.SegmentContainer ?? "ts";
|
||||
|
||||
// http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
|
||||
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
|
||||
var hlsVersion = isHlsInFmp4 ? "7" : "3";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
|
||||
builder.AppendLine("#EXTM3U")
|
||||
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||
.AppendLine("#EXT-X-VERSION:3")
|
||||
.Append("#EXT-X-VERSION:")
|
||||
.Append(hlsVersion)
|
||||
.AppendLine()
|
||||
.Append("#EXT-X-TARGETDURATION:")
|
||||
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
|
||||
.AppendLine()
|
||||
@ -1151,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
|
||||
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
|
||||
var queryString = Request.QueryString;
|
||||
|
||||
if (isHlsInFmp4)
|
||||
{
|
||||
builder.Append("#EXT-X-MAP:URI=\"")
|
||||
.Append("hls1/")
|
||||
.Append(name)
|
||||
.Append("/-1")
|
||||
.Append(segmentExtension)
|
||||
.Append(queryString)
|
||||
.Append('"')
|
||||
.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var length in segmentLengths)
|
||||
{
|
||||
builder.Append("#EXTINF:")
|
||||
@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
|
||||
_dlnaManager,
|
||||
_deviceManager,
|
||||
_transcodingJobHelper,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
@ -1208,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (System.IO.File.Exists(segmentPath))
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
@ -1222,7 +1246,7 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
if (System.IO.File.Exists(segmentPath))
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
transcodingLock.Release();
|
||||
released = true;
|
||||
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
|
||||
@ -1233,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
|
||||
if (currentTranscodingIndex == null)
|
||||
if (segmentId == -1)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
|
||||
startTranscoding = true;
|
||||
segmentId = 0;
|
||||
}
|
||||
else if (currentTranscodingIndex == null)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
|
||||
startTranscoding = true;
|
||||
@ -1265,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
|
||||
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
|
||||
|
||||
state.WaitForPath = segmentPath;
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
job = await _transcodingJobHelper.StartFfMpeg(
|
||||
state,
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
|
||||
GetCommandLineArguments(playlistPath, state, true, segmentId),
|
||||
Request,
|
||||
_transcodingJobType,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
@ -1284,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
if (job?.TranscodingThrottler != null)
|
||||
{
|
||||
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
|
||||
@ -1301,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
_logger.LogDebug("returning {0} [general case]", segmentPath);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@ -1325,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
|
||||
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec); // GetNumberOfThreads is static.
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
|
||||
|
||||
if (state.BaseRequest.BreakOnNonKeyFrames)
|
||||
{
|
||||
@ -1341,36 +1369,57 @@ namespace Jellyfin.Api.Controllers
|
||||
state.BaseRequest.BreakOnNonKeyFrames = false;
|
||||
}
|
||||
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
|
||||
|
||||
// If isEncoding is true we're actually starting ffmpeg
|
||||
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
|
||||
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
|
||||
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
||||
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
|
||||
var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
var outputTsArg = outputPrefix + "%d" + outputExtension;
|
||||
|
||||
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
|
||||
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
|
||||
var segmentFormat = outputExtension.TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
|
||||
? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid HLS segment container: " + segmentFormat);
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
|
||||
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
: "128";
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
|
||||
inputModifier,
|
||||
_encodingHelper.GetInputArgument(state, encodingOptions),
|
||||
_encodingHelper.GetInputArgument(state, _encodingOptions),
|
||||
threads,
|
||||
mapArgs,
|
||||
GetVideoArguments(state, encodingOptions, startNumber),
|
||||
GetAudioArguments(state, encodingOptions),
|
||||
GetVideoArguments(state, startNumber),
|
||||
GetAudioArguments(state),
|
||||
maxMuxingQueueSize,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||
segmentFormat,
|
||||
@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
|
||||
outputPath).Trim();
|
||||
}
|
||||
|
||||
private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
|
||||
/// <summary>
|
||||
/// Gets the audio arguments for transcoding.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <returns>The command line arguments for audio transcoding.</returns>
|
||||
private string GetAudioArguments(StreamState state)
|
||||
{
|
||||
if (state.AudioStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return "-acodec copy";
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = new List<string>();
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams.Add("-acodec " + audioCodec);
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioChannels.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams.Add("-vn");
|
||||
return string.Join(' ', audioTranscodeParams);
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
||||
{
|
||||
return "-codec:a:0 copy -copypriorss:a:0 0";
|
||||
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
|
||||
}
|
||||
|
||||
return "-codec:a:0 copy";
|
||||
return "-codec:a:0 copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
@ -1446,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
|
||||
/// <summary>
|
||||
/// Gets the video arguments for transcoding.
|
||||
/// </summary>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <param name="startNumber">The first number in the hls sequence.</param>
|
||||
/// <returns>The command line arguments for video transcoding.</returns>
|
||||
private string GetVideoArguments(StreamState state, int startNumber)
|
||||
{
|
||||
if (state.VideoStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
|
||||
var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
// {
|
||||
// args += " -mpegts_m2ts_mode 1";
|
||||
// }
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
// See if we can save come cpu cycles by avoiding encoding.
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
{
|
||||
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
if (!string.IsNullOrEmpty(bitStreamArgs))
|
||||
{
|
||||
args += " " + bitStreamArgs;
|
||||
}
|
||||
}
|
||||
|
||||
args += " -start_at_zero";
|
||||
|
||||
// args += " -flags -global_header";
|
||||
}
|
||||
else
|
||||
{
|
||||
var gopArg = string.Empty;
|
||||
var keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
|
||||
startNumber * state.SegmentLength,
|
||||
state.SegmentLength);
|
||||
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
|
||||
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
// Set the key frame params for video encoding to match the hls segment time.
|
||||
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
|
||||
|
||||
if (framerate.HasValue)
|
||||
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
|
||||
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// This is to make sure keyframe interval is limited to our segment,
|
||||
// as forcing keyframes is not enough.
|
||||
// Example: we encoded half of desired length, then codec detected
|
||||
// scene cut and inserted a keyframe; next forced keyframe would
|
||||
// be created outside of segment, which breaks seeking
|
||||
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
|
||||
gopArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -g {0} -keyint_min {0} -sc_threshold 0",
|
||||
Math.Ceiling(state.SegmentLength * framerate.Value));
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
|
||||
|
||||
// Unable to force key frames using these hw encoders, set key frames by GOP
|
||||
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " " + gopArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
args += " " + keyFrameArg + gopArg;
|
||||
args += " -bf 0";
|
||||
}
|
||||
|
||||
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
// This is for graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
|
||||
// Graphical subs overlay and resolution params.
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// Add resolution params, if specified
|
||||
else
|
||||
{
|
||||
args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
|
||||
// Resolution params.
|
||||
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// -start_at_zero is necessary to use with -ss when seeking,
|
||||
@ -1693,7 +1750,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
|
||||
{
|
||||
var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
|
||||
var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
|
||||
|
||||
if (job == null || job.HasExited)
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? mediaTypes)
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
|
||||
{
|
||||
var parentItem = string.IsNullOrEmpty(parentId)
|
||||
? null
|
||||
@ -61,10 +62,11 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
@ -78,8 +80,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var query = new InternalItemsQuery
|
||||
{
|
||||
User = user,
|
||||
MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
|
||||
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
|
||||
MediaTypes = mediaTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
Recursive = true,
|
||||
EnableTotalRecordCount = false,
|
||||
DtoOptions = new DtoOptions
|
||||
@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryFilters> GetQueryFilters(
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isAiring,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSports,
|
||||
@ -156,10 +158,11 @@ namespace Jellyfin.Api.Controllers
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentItem = null;
|
||||
}
|
||||
@ -167,8 +170,7 @@ namespace Jellyfin.Api.Controllers
|
||||
var filters = new QueryFilters();
|
||||
var genreQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
IncludeItemTypes =
|
||||
(includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
Fields = Array.Empty<ItemFields>(),
|
||||
@ -192,10 +194,11 @@ namespace Jellyfin.Api.Controllers
|
||||
genreQuery.Parent = parentItem;
|
||||
}
|
||||
|
||||
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes[0], nameof(Audio), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
|
||||
result = _libraryManager.GetGenres(query);
|
||||
}
|
||||
|
||||
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
@ -112,11 +112,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="segmentId">The segment id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <response code="200">Hls video segment returned.</response>
|
||||
/// <response code="404">Hls segment not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
|
||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||
// [Authenticated]
|
||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesVideoFile]
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsVideoSegmentLegacy(
|
||||
@ -132,13 +134,25 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var normalizedPlaylistId = playlistId;
|
||||
|
||||
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath)
|
||||
.FirstOrDefault(i =>
|
||||
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase)
|
||||
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid.");
|
||||
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
||||
// Add . to start of segment container for future use.
|
||||
segmentContainer = segmentContainer.Insert(0, ".");
|
||||
string? playlistPath = null;
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
var pathExtension = Path.GetExtension(path);
|
||||
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
playlistPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return GetFileResult(file, playlistPath);
|
||||
return playlistPath == null
|
||||
? NotFound("Hls segment not found.")
|
||||
: GetFileResult(file, playlistPath);
|
||||
}
|
||||
|
||||
private ActionResult GetFileResult(string path, string playlistPath)
|
||||
|
@ -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,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer,
|
||||
[FromRoute] int? imageIndex = null)
|
||||
[FromQuery] int? imageIndex)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
@ -974,6 +1507,103 @@ namespace Jellyfin.Api.Controllers
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get user profile image.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="imageType">Image type.</param>
|
||||
/// <param name="imageIndex">Image index.</param>
|
||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||
/// <param name="maxWidth">The maximum image width to return.</param>
|
||||
/// <param name="maxHeight">The maximum image height to return.</param>
|
||||
/// <param name="percentPlayed">Optional. Percent to render for the percent played overlay.</param>
|
||||
/// <param name="unplayedCount">Optional. Unplayed count overlay to render.</param>
|
||||
/// <param name="width">The fixed image width to return.</param>
|
||||
/// <param name="height">The fixed image height to return.</param>
|
||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||
/// <param name="blur">Optional. Blur image.</param>
|
||||
/// <param name="backgroundColor">Optional. Apply a background color for transparent images.</param>
|
||||
/// <param name="foregroundLayer">Optional. Apply a foreground layer on top of the image.</param>
|
||||
/// <response code="200">Image stream returned.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>
|
||||
/// A <see cref="FileStreamResult"/> containing the file stream on success,
|
||||
/// or a <see cref="NotFoundResult"/> if item not found.
|
||||
/// </returns>
|
||||
[HttpGet("Users/{userId}/Images/{imageType}/{imageIndex}")]
|
||||
[HttpHead("Users/{userId}/Images/{imageType}/{imageIndex}", Name = "HeadUserImageByIndex")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesImageFile]
|
||||
public async Task<ActionResult> GetUserImageByIndex(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] ImageType imageType,
|
||||
[FromRoute, Required] int imageIndex,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] ImageFormat? format,
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] double? percentPlayed,
|
||||
[FromQuery] int? unplayedCount,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? quality,
|
||||
[FromQuery] bool? cropWhitespace,
|
||||
[FromQuery] bool? addPlayedIndicator,
|
||||
[FromQuery] int? blur,
|
||||
[FromQuery] string? backgroundColor,
|
||||
[FromQuery] string? foregroundLayer)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user?.ProfileImage == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var info = new ItemImageInfo
|
||||
{
|
||||
Path = user.ProfileImage.Path,
|
||||
Type = ImageType.Profile,
|
||||
DateModified = user.ProfileImage.LastModified
|
||||
};
|
||||
|
||||
if (width.HasValue)
|
||||
{
|
||||
info.Width = width.Value;
|
||||
}
|
||||
|
||||
if (height.HasValue)
|
||||
{
|
||||
info.Height = height.Value;
|
||||
}
|
||||
|
||||
return await GetImageInternal(
|
||||
user.Id,
|
||||
imageType,
|
||||
imageIndex,
|
||||
tag,
|
||||
format,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
percentPlayed,
|
||||
unplayedCount,
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
cropWhitespace,
|
||||
addPlayedIndicator,
|
||||
blur,
|
||||
backgroundColor,
|
||||
foregroundLayer,
|
||||
null,
|
||||
Request.Method.Equals(HttpMethods.Head, StringComparison.OrdinalIgnoreCase),
|
||||
info)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
|
@ -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,
|
||||
@ -159,7 +156,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery] string? locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
@ -173,7 +170,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] string? excludeItemIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
@ -181,34 +178,34 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? artists,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] string? artistIds,
|
||||
[FromQuery] string? albumArtistIds,
|
||||
[FromQuery] string? contributingArtistIds,
|
||||
[FromQuery] string? albums,
|
||||
[FromQuery] string? albumIds,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] string? videoTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
@ -219,18 +216,15 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery] string? seriesStatus,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
// use user id route parameter over query parameter
|
||||
userId = uId ?? userId;
|
||||
|
||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||
? _userManager.GetUserById(userId.Value)
|
||||
: null;
|
||||
@ -238,8 +232,9 @@ namespace Jellyfin.Api.Controllers
|
||||
.AddClientFields(Request)
|
||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||
|
||||
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase))
|
||||
if (includeItemTypes.Length == 1
|
||||
&& (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
|
||||
|| includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
parentId = null;
|
||||
}
|
||||
@ -262,7 +257,7 @@ namespace Jellyfin.Api.Controllers
|
||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
recursive = true;
|
||||
includeItemTypes = "Playlist";
|
||||
includeItemTypes = new[] { "Playlist" };
|
||||
}
|
||||
|
||||
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
|
||||
@ -291,14 +286,14 @@ namespace Jellyfin.Api.Controllers
|
||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||
}
|
||||
|
||||
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder))
|
||||
if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
|
||||
{
|
||||
var query = new InternalItemsQuery(user!)
|
||||
{
|
||||
IsPlayed = isPlayed,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
MediaTypes = mediaTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
Recursive = recursive ?? false,
|
||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||
IsFavorite = isFavorite,
|
||||
@ -330,28 +325,28 @@ namespace Jellyfin.Api.Controllers
|
||||
HasTrailer = hasTrailer,
|
||||
IsHD = isHd,
|
||||
Is4K = is4K,
|
||||
Tags = RequestHelpers.Split(tags, '|', true),
|
||||
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true),
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
ArtistIds = RequestHelpers.GetGuids(artistIds),
|
||||
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds),
|
||||
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds),
|
||||
StudioIds = RequestHelpers.GetGuids(studioIds),
|
||||
Tags = tags,
|
||||
OfficialRatings = officialRatings,
|
||||
Genres = genres,
|
||||
ArtistIds = artistIds,
|
||||
AlbumArtistIds = albumArtistIds,
|
||||
ContributingArtistIds = contributingArtistIds,
|
||||
GenreIds = genreIds,
|
||||
StudioIds = studioIds,
|
||||
Person = person,
|
||||
PersonIds = RequestHelpers.GetGuids(personIds),
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(),
|
||||
PersonIds = personIds,
|
||||
PersonTypes = personTypes,
|
||||
Years = years,
|
||||
ImageTypes = imageTypes,
|
||||
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(),
|
||||
VideoTypes = videoTypes,
|
||||
AdjacentTo = adjacentTo,
|
||||
ItemIds = RequestHelpers.GetGuids(ids),
|
||||
ItemIds = ids,
|
||||
MinCommunityRating = minCommunityRating,
|
||||
MinCriticRating = minCriticRating,
|
||||
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
|
||||
ParentIndexNumber = parentIndexNumber,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds),
|
||||
ExcludeItemIds = excludeItemIds,
|
||||
DtoOptions = dtoOptions,
|
||||
SearchTerm = searchTerm,
|
||||
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
|
||||
@ -360,7 +355,7 @@ namespace Jellyfin.Api.Controllers
|
||||
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
query.CollapseBoxSetItems = false;
|
||||
}
|
||||
@ -400,9 +395,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Filter by Series Status
|
||||
if (!string.IsNullOrEmpty(seriesStatus))
|
||||
if (seriesStatus.Length != 0)
|
||||
{
|
||||
query.SeriesStatuses = seriesStatus.Split(',').Select(d => (SeriesStatus)Enum.Parse(typeof(SeriesStatus), d, true)).ToArray();
|
||||
query.SeriesStatuses = seriesStatus;
|
||||
}
|
||||
|
||||
// ExcludeLocationTypes
|
||||
@ -411,13 +406,9 @@ namespace Jellyfin.Api.Controllers
|
||||
query.IsVirtualItem = false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(locationTypes))
|
||||
if (locationTypes.Length > 0 && locationTypes.Length < 4)
|
||||
{
|
||||
var requestedLocationTypes = locationTypes.Split(',');
|
||||
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
|
||||
{
|
||||
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
|
||||
}
|
||||
query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
|
||||
}
|
||||
|
||||
// Min official rating
|
||||
@ -433,9 +424,9 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// Artists
|
||||
if (!string.IsNullOrEmpty(artists))
|
||||
if (artists.Length != 0)
|
||||
{
|
||||
query.ArtistIds = artists.Split('|').Select(i =>
|
||||
query.ArtistIds = artists.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -449,29 +440,29 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrWhiteSpace(excludeArtistIds))
|
||||
if (excludeArtistIds.Length != 0)
|
||||
{
|
||||
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
|
||||
query.ExcludeArtistIds = excludeArtistIds;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(albumIds))
|
||||
if (albumIds.Length != 0)
|
||||
{
|
||||
query.AlbumIds = RequestHelpers.GetGuids(albumIds);
|
||||
query.AlbumIds = albumIds;
|
||||
}
|
||||
|
||||
// Albums
|
||||
if (!string.IsNullOrEmpty(albums))
|
||||
if (albums.Length != 0)
|
||||
{
|
||||
query.AlbumIds = albums.Split('|').SelectMany(i =>
|
||||
query.AlbumIds = albums.SelectMany(i =>
|
||||
{
|
||||
return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
// Studios
|
||||
if (!string.IsNullOrEmpty(studios))
|
||||
if (studios.Length != 0)
|
||||
{
|
||||
query.StudioIds = studios.Split('|').Select(i =>
|
||||
query.StudioIds = studios.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -505,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>
|
||||
@ -533,12 +775,12 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
@ -569,13 +811,13 @@ namespace Jellyfin.Api.Controllers
|
||||
ParentId = parentIdGuid,
|
||||
Recursive = true,
|
||||
DtoOptions = dtoOptions,
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
MediaTypes = mediaTypes,
|
||||
IsVirtualItem = false,
|
||||
CollapseBoxSetItems = false,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
AncestorIds = ancestorIds,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
SearchTerm = searchTerm
|
||||
});
|
||||
|
||||
|
@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult DeleteItems([FromQuery] string? ids)
|
||||
public ActionResult DeleteItems([FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ids))
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
var itemIds = RequestHelpers.Split(ids, ',', true);
|
||||
foreach (var i in itemIds)
|
||||
foreach (var i in ids)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(i);
|
||||
var auth = _authContext.GetAuthorizationInfo(Request);
|
||||
@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||
@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
|
||||
};
|
||||
|
||||
// ExcludeArtistIds
|
||||
if (!string.IsNullOrEmpty(excludeArtistIds))
|
||||
if (excludeArtistIds.Length != 0)
|
||||
{
|
||||
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds);
|
||||
query.ExcludeArtistIds = excludeArtistIds;
|
||||
}
|
||||
|
||||
List<BaseItem> itemsResult = _libraryManager.GetItemList(query);
|
||||
|
@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||
[FromQuery] SortOrder? sortOrder,
|
||||
[FromQuery] bool enableFavoriteSorting = false,
|
||||
[FromQuery] bool addCurrentProgram = true)
|
||||
@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsNews = isNews,
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
SortBy = RequestHelpers.Split(sortBy, ',', true),
|
||||
SortBy = sortBy,
|
||||
SortOrder = sortOrder ?? SortOrder.Ascending,
|
||||
AddCurrentProgram = addCurrentProgram
|
||||
},
|
||||
@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
|
||||
[FromQuery] string? channelIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] DateTime? minStartDate,
|
||||
[FromQuery] bool? hasAired,
|
||||
@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ChannelIds = RequestHelpers.Split(channelIds, ',', true)
|
||||
.Select(i => new Guid(i)).ToArray(),
|
||||
ChannelIds = channelIds,
|
||||
HasAired = hasAired,
|
||||
IsAiring = isAiring,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IsKids = isKids,
|
||||
IsSports = isSports,
|
||||
SeriesTimerId = seriesTimerId,
|
||||
Genres = RequestHelpers.Split(genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||
Genres = genres,
|
||||
GenreIds = genreIds
|
||||
};
|
||||
|
||||
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
|
||||
@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true)
|
||||
.Select(i => new Guid(i)).ToArray(),
|
||||
ChannelIds = body.ChannelIds,
|
||||
HasAired = body.HasAired,
|
||||
IsAiring = body.IsAiring,
|
||||
EnableTotalRecordCount = body.EnableTotalRecordCount,
|
||||
@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers
|
||||
IsKids = body.IsKids,
|
||||
IsSports = body.IsSports,
|
||||
SeriesTimerId = body.SeriesTimerId,
|
||||
Genres = RequestHelpers.Split(body.Genres, '|', true),
|
||||
GenreIds = RequestHelpers.GetGuids(body.GenreIds)
|
||||
Genres = body.Genres,
|
||||
GenreIds = body.GenreIds
|
||||
};
|
||||
|
||||
if (!body.LibrarySeriesId.Equals(Guid.Empty))
|
||||
@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] bool enableTotalRecordCount = true)
|
||||
@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers
|
||||
IsNews = isNews,
|
||||
IsSports = isSports,
|
||||
EnableTotalRecordCount = enableTotalRecordCount,
|
||||
GenreIds = RequestHelpers.GetGuids(genreIds)
|
||||
GenreIds = genreIds
|
||||
};
|
||||
|
||||
var dtoOptions = new DtoOptions { Fields = fields }
|
||||
|
@ -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;
|
||||
@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var result = _libraryManager.GetMusicGenres(query);
|
||||
|
||||
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes);
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? excludePersonTypes,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery] string? appearsInItemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
|
||||
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
|
||||
{
|
||||
PersonTypes = RequestHelpers.Split(personTypes, ',', true),
|
||||
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true),
|
||||
PersonTypes = personTypes,
|
||||
ExcludePersonTypes = excludePersonTypes,
|
||||
NameContains = searchTerm,
|
||||
User = user,
|
||||
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
|
||||
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
||||
[FromBody, Required] CreatePlaylistDto createPlaylistRequest)
|
||||
{
|
||||
Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
|
||||
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
|
||||
{
|
||||
Name = createPlaylistRequest.Name,
|
||||
ItemIdList = idGuidArray,
|
||||
ItemIdList = createPlaylistRequest.Ids,
|
||||
UserId = createPlaylistRequest.UserId,
|
||||
MediaType = createPlaylistRequest.MediaType
|
||||
}).ConfigureAwait(false);
|
||||
@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> AddToPlaylist(
|
||||
[FromRoute, Required] Guid playlistId,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery] Guid? userId)
|
||||
{
|
||||
await _playlistManager.AddToPlaylistAsync(playlistId, RequestHelpers.GetGuids(ids), userId ?? Guid.Empty).ConfigureAwait(false);
|
||||
await _playlistManager.AddToPlaylistAsync(playlistId, ids, userId ?? Guid.Empty).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||
[HttpDelete("{playlistId}/Items")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public async Task<ActionResult> RemoveFromPlaylist([FromRoute, Required] string playlistId, [FromQuery] string? entryIds)
|
||||
public async Task<ActionResult> RemoveFromPlaylist(
|
||||
[FromRoute, Required] string playlistId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] entryIds)
|
||||
{
|
||||
await _playlistManager.RemoveFromPlaylistAsync(playlistId, RequestHelpers.Split(entryIds, ',', true)).ConfigureAwait(false);
|
||||
await _playlistManager.RemoveFromPlaylistAsync(playlistId, entryIds).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery, Required] string searchTerm,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery] bool? isMovie,
|
||||
[FromQuery] bool? isSeries,
|
||||
@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
|
||||
IncludeStudios = includeStudios,
|
||||
StartIndex = startIndex,
|
||||
UserId = userId ?? Guid.Empty,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true),
|
||||
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
ParentId = parentId,
|
||||
|
||||
IsKids = isKids,
|
||||
|
@ -6,6 +6,7 @@ using System.Threading;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.SessionDtos;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@ -160,12 +161,12 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult Play(
|
||||
[FromRoute, Required] string sessionId,
|
||||
[FromQuery, Required] PlayCommand playCommand,
|
||||
[FromQuery, Required] string itemIds,
|
||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
||||
[FromQuery] long? startPositionTicks)
|
||||
{
|
||||
var playRequest = new PlayRequest
|
||||
{
|
||||
ItemIds = RequestHelpers.GetGuids(itemIds),
|
||||
ItemIds = itemIds,
|
||||
StartPositionTicks = startPositionTicks,
|
||||
PlayCommand = playCommand
|
||||
};
|
||||
@ -378,7 +379,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromQuery] string? playableMediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
|
||||
[FromQuery] bool supportsMediaControl = false,
|
||||
[FromQuery] bool supportsSync = false,
|
||||
@ -391,7 +392,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
_sessionManager.ReportCapabilities(id, new ClientCapabilities
|
||||
{
|
||||
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true),
|
||||
PlayableMediaTypes = playableMediaTypes,
|
||||
SupportedCommands = supportedCommands,
|
||||
SupportsMediaControl = supportsMediaControl,
|
||||
SupportsSync = supportsSync,
|
||||
@ -412,14 +413,14 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult PostFullCapabilities(
|
||||
[FromQuery] string? id,
|
||||
[FromBody, Required] ClientCapabilities capabilities)
|
||||
[FromBody, Required] ClientCapabilitiesDto capabilities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
id = RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id;
|
||||
}
|
||||
|
||||
_sessionManager.ReportCapabilities(id, capabilities);
|
||||
_sessionManager.ReportCapabilities(id, capabilities.ToClientCapabilities());
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
@ -72,9 +72,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult UpdateInitialConfiguration([FromBody, Required] StartupConfigurationDto startupConfiguration)
|
||||
{
|
||||
_config.Configuration.UICulture = startupConfiguration.UICulture;
|
||||
_config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode;
|
||||
_config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage;
|
||||
_config.Configuration.UICulture = startupConfiguration.UICulture ?? string.Empty;
|
||||
_config.Configuration.MetadataCountryCode = startupConfiguration.MetadataCountryCode ?? string.Empty;
|
||||
_config.Configuration.PreferredMetadataLanguage = startupConfiguration.PreferredMetadataLanguage ?? string.Empty;
|
||||
_config.SaveConfiguration();
|
||||
return NoContent();
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? searchTerm,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
IsFavorite = isFavorite,
|
||||
@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
var result = _libraryManager.GetStudios(query);
|
||||
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes);
|
||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||
}
|
||||
|
||||
|
@ -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,9 +1,10 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] string? mediaType,
|
||||
[FromQuery] string? type,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool enableTotalRecordCount = false)
|
||||
@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers
|
||||
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
|
||||
{
|
||||
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
|
||||
MediaTypes = RequestHelpers.Split(mediaType!, ',', true),
|
||||
IncludeItemTypes = RequestHelpers.Split(type!, ',', true),
|
||||
MediaTypes = mediaType,
|
||||
IncludeItemTypes = type,
|
||||
IsVirtualItem = false,
|
||||
StartIndex = startIndex,
|
||||
Limit = limit,
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasParentalRating,
|
||||
[FromQuery] bool? isHd,
|
||||
[FromQuery] bool? is4K,
|
||||
[FromQuery] string? locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
|
||||
[FromQuery] bool? isMissing,
|
||||
[FromQuery] bool? isUnaired,
|
||||
@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] bool? hasImdbId,
|
||||
[FromQuery] bool? hasTmdbId,
|
||||
[FromQuery] bool? hasTvdbId,
|
||||
[FromQuery] string? excludeItemIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery] bool? recursive,
|
||||
@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||
[FromQuery] bool? isFavorite,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] string? genres,
|
||||
[FromQuery] string? officialRatings,
|
||||
[FromQuery] string? tags,
|
||||
[FromQuery] string? years,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||
[FromQuery] string? person,
|
||||
[FromQuery] string? personIds,
|
||||
[FromQuery] string? personTypes,
|
||||
[FromQuery] string? studios,
|
||||
[FromQuery] string? artists,
|
||||
[FromQuery] string? excludeArtistIds,
|
||||
[FromQuery] string? artistIds,
|
||||
[FromQuery] string? albumArtistIds,
|
||||
[FromQuery] string? contributingArtistIds,
|
||||
[FromQuery] string? albums,
|
||||
[FromQuery] string? albumIds,
|
||||
[FromQuery] string? ids,
|
||||
[FromQuery] string? videoTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
|
||||
[FromQuery] string? minOfficialRating,
|
||||
[FromQuery] bool? isLocked,
|
||||
[FromQuery] bool? isPlaceHolder,
|
||||
@ -184,20 +184,19 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] int? maxWidth,
|
||||
[FromQuery] int? maxHeight,
|
||||
[FromQuery] bool? is3D,
|
||||
[FromQuery] string? seriesStatus,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
|
||||
[FromQuery] string? nameStartsWithOrGreater,
|
||||
[FromQuery] string? nameStartsWith,
|
||||
[FromQuery] string? nameLessThan,
|
||||
[FromQuery] string? studioIds,
|
||||
[FromQuery] string? genreIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||
[FromQuery] bool enableTotalRecordCount = true,
|
||||
[FromQuery] bool? enableImages = true)
|
||||
{
|
||||
var includeItemTypes = "Trailer";
|
||||
var includeItemTypes = new[] { "Trailer" };
|
||||
|
||||
return _itemsController
|
||||
.GetItems(
|
||||
userId,
|
||||
userId,
|
||||
maxOfficialRating,
|
||||
hasThemeSong,
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[ProducesAudioFile]
|
||||
public async Task<ActionResult> GetUniversalAudioStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] Guid? userId,
|
||||
@ -191,8 +192,11 @@ namespace Jellyfin.Api.Controllers
|
||||
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation
|
||||
// ffmpeg option -> file extension
|
||||
// mpegts -> ts
|
||||
// fmp4 -> mp4
|
||||
// TODO: remove this when we switch back to the segment muxer
|
||||
var supportedHlsContainers = new[] { "mpegts", "fmp4" };
|
||||
var supportedHlsContainers = new[] { "ts", "mp4" };
|
||||
|
||||
var dynamicHlsRequestDto = new HlsAudioRequestDto
|
||||
{
|
||||
@ -201,7 +205,7 @@ namespace Jellyfin.Api.Controllers
|
||||
Static = isStatic,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
// fallback to mpegts if device reports some weird value unsupported by hls
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "mpegts",
|
||||
SegmentContainer = Array.Exists(supportedHlsContainers, element => element == transcodingContainer) ? transcodingContainer : "ts",
|
||||
MediaSourceId = mediaSourceId,
|
||||
DeviceId = deviceId,
|
||||
AudioCodec = audioCodec,
|
||||
@ -258,7 +262,7 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
|
||||
private DeviceProfile GetDeviceProfile(
|
||||
string? container,
|
||||
string[] containers,
|
||||
string? transcodingContainer,
|
||||
string? audioCodec,
|
||||
string? transcodingProtocol,
|
||||
@ -270,7 +274,6 @@ namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
var deviceProfile = new DeviceProfile();
|
||||
|
||||
var containers = RequestHelpers.Split(container, ',', true);
|
||||
int len = containers.Length;
|
||||
var directPlayProfiles = new DirectPlayProfile[len];
|
||||
for (int i = 0; i < len; i++)
|
||||
@ -327,7 +330,7 @@ namespace Jellyfin.Api.Controllers
|
||||
if (conditions.Count > 0)
|
||||
{
|
||||
// codec profile
|
||||
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = container, Conditions = conditions.ToArray() });
|
||||
codecProfiles.Add(new CodecProfile { Type = CodecType.Audio, Container = string.Join(',', containers), Conditions = conditions.ToArray() });
|
||||
}
|
||||
|
||||
deviceProfile.CodecProfiles = codecProfiles.ToArray();
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] Guid? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery] bool? isPlayed,
|
||||
[FromQuery] bool? enableImages,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
|
||||
new LatestItemsQuery
|
||||
{
|
||||
GroupItems = groupItems,
|
||||
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
IsPlayed = isPlayed,
|
||||
Limit = limit,
|
||||
ParentId = parentId ?? Guid.Empty,
|
||||
|
@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.UserViewDtos;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromQuery] bool? includeExternalContent,
|
||||
[FromQuery] string? presetViews,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
|
||||
[FromQuery] bool includeHidden = false)
|
||||
{
|
||||
var query = new UserViewQuery
|
||||
@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
|
||||
query.IncludeExternalContent = includeExternalContent.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(presetViews))
|
||||
if (presetViews.Length != 0)
|
||||
{
|
||||
query.PresetViews = RequestHelpers.Split(presetViews, ',', true);
|
||||
query.PresetViews = presetViews;
|
||||
}
|
||||
|
||||
var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;
|
||||
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@ -43,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(
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TranscodingJobDto? job = null;
|
||||
var playlist = state.OutputFilePath;
|
||||
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
|
||||
|
||||
if (!System.IO.File.Exists(playlist))
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
|
||||
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (!System.IO.File.Exists(playlist))
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
job = await _transcodingJobHelper.StartFfMpeg(
|
||||
state,
|
||||
playlist,
|
||||
GetCommandLineArguments(playlist, state),
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, state),
|
||||
Request,
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource)
|
||||
@ -328,7 +329,7 @@ namespace Jellyfin.Api.Controllers
|
||||
minSegments = state.MinSegments;
|
||||
if (minSegments > 0)
|
||||
{
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -338,14 +339,14 @@ namespace Jellyfin.Api.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
|
||||
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
|
||||
if (job != null)
|
||||
{
|
||||
_transcodingJobHelper.OnTranscodeEndRequest(job);
|
||||
}
|
||||
|
||||
var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
|
||||
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
|
||||
|
||||
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
@ -361,15 +362,44 @@ namespace Jellyfin.Api.Controllers
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
|
||||
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
|
||||
var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
|
||||
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
|
||||
|
||||
var segmentFormat = format.TrimStart('.');
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
|
||||
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
var outputTsArg = outputPrefix + "%d" + outputExtension;
|
||||
|
||||
var segmentFormat = outputExtension.TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
segmentFormat = "mpegts";
|
||||
}
|
||||
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var outputFmp4HeaderArg = string.Empty;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
// on Windows, the path of fmp4 header file needs to be configured
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
|
||||
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
|
||||
}
|
||||
|
||||
segmentFormat = "fmp4" + outputFmp4HeaderArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
|
||||
}
|
||||
|
||||
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
|
||||
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
|
||||
: "128";
|
||||
|
||||
var baseUrlParam = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@ -378,20 +408,19 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"",
|
||||
inputModifier,
|
||||
_encodingHelper.GetInputArgument(state, _encodingOptions),
|
||||
threads,
|
||||
_encodingHelper.GetMapArgs(state),
|
||||
mapArgs,
|
||||
GetVideoArguments(state),
|
||||
GetAudioArguments(state),
|
||||
maxMuxingQueueSize,
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||
string.Empty,
|
||||
segmentFormat,
|
||||
baseUrlParam,
|
||||
outputPath,
|
||||
outputTsArg)
|
||||
.Trim();
|
||||
outputTsArg,
|
||||
outputPath).Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>The command line arguments for audio transcoding.</returns>
|
||||
private string GetAudioArguments(StreamState state)
|
||||
{
|
||||
var codec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
if (state.AudioStream == null)
|
||||
{
|
||||
return "-codec:a:0 copy";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + codec;
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec;
|
||||
|
||||
if (state.OutputAudioBitrate.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioChannels.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (state.OutputAudioSampleRate.HasValue)
|
||||
{
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var args = "-codec:a:0 " + audioCodec;
|
||||
|
||||
var channels = state.OutputAudioChannels;
|
||||
|
||||
@ -429,7 +497,7 @@ namespace Jellyfin.Api.Controllers
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
|
||||
|
||||
return args;
|
||||
}
|
||||
@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <returns>The command line arguments for video transcoding.</returns>
|
||||
private string GetVideoArguments(StreamState state)
|
||||
{
|
||||
if (state.VideoStream == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
return string.Empty;
|
||||
@ -450,46 +523,64 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
var args = "-codec:v:0 " + codec;
|
||||
|
||||
// Prefer hvc1 to hev1.
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -tag:v:0 hvc1";
|
||||
}
|
||||
|
||||
// if (state.EnableMpegtsM2TsMode)
|
||||
// {
|
||||
// args += " -mpegts_m2ts_mode 1";
|
||||
// }
|
||||
|
||||
// See if we can save come cpu cycles by avoiding encoding
|
||||
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
|
||||
// See if we can save come cpu cycles by avoiding encoding.
|
||||
if (EncodingHelper.IsCopyCodec(codec))
|
||||
{
|
||||
// if h264_mp4toannexb is ever added, do not use it for live tv
|
||||
if (state.VideoStream != null &&
|
||||
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
// If h264_mp4toannexb is ever added, do not use it for live tv.
|
||||
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
|
||||
if (!string.IsNullOrEmpty(bitStreamArgs))
|
||||
{
|
||||
args += " " + bitStreamArgs;
|
||||
}
|
||||
}
|
||||
|
||||
args += " -start_at_zero";
|
||||
}
|
||||
else
|
||||
{
|
||||
var keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
|
||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
|
||||
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
|
||||
|
||||
// Set the key frame params for video encoding to match the hls segment time.
|
||||
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
|
||||
|
||||
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
|
||||
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " -bf 0";
|
||||
}
|
||||
|
||||
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
|
||||
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
|
||||
|
||||
// Add resolution params, if specified
|
||||
if (!hasGraphicalSubs)
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// Graphical subs overlay and resolution params.
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Resolution params.
|
||||
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
|
||||
}
|
||||
|
||||
// This is for internal graphical subs
|
||||
if (hasGraphicalSubs)
|
||||
if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
|
||||
{
|
||||
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
|
||||
args += " -start_at_zero";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult> MergeVersions([FromQuery, Required] string itemIds)
|
||||
public async Task<ActionResult> MergeVersions([FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds)
|
||||
{
|
||||
var items = RequestHelpers.Split(itemIds, ',', true)
|
||||
var items = itemIds
|
||||
.Select(i => _libraryManager.GetItemById(i))
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.Id)
|
||||
@ -326,15 +327,13 @@ namespace Jellyfin.Api.Controllers
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container?}", Name = "GetVideoStreamWithExt")]
|
||||
[HttpGet("{itemId}/stream")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container?}", Name = "HeadVideoStreamWithExt")]
|
||||
[HttpHead("{itemId}/stream", Name = "HeadVideoStream")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public async Task<ActionResult> GetVideoStream(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute] string? container,
|
||||
[FromQuery] string? container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
@ -529,5 +528,166 @@ namespace Jellyfin.Api.Controllers
|
||||
_transcodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a video stream.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="container">The video container. Possible values are: ts, webm, asf, wmv, ogv, mp4, m4v, mkv, mpeg, mpg, avi, 3gp, wmv, wtv, m2ts, mov, iso, flv. </param>
|
||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||
/// <param name="params">The streaming parameters.</param>
|
||||
/// <param name="tag">The tag.</param>
|
||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <param name="segmentContainer">The segment container.</param>
|
||||
/// <param name="segmentLength">The segment lenght.</param>
|
||||
/// <param name="minSegments">The minimum number of segments.</param>
|
||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||
/// <param name="maxRefFrames">Optional.</param>
|
||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||
/// <param name="liveStreamId">The live stream id.</param>
|
||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.</param>
|
||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||
/// <param name="transcodingReasons">Optional. The transcoding reason.</param>
|
||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||
/// <response code="200">Video stream returned.</response>
|
||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesVideoFile]
|
||||
public Task<ActionResult> GetVideoStreamByContainer(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string container,
|
||||
[FromQuery] bool? @static,
|
||||
[FromQuery] string? @params,
|
||||
[FromQuery] string? tag,
|
||||
[FromQuery] string? deviceProfileId,
|
||||
[FromQuery] string? playSessionId,
|
||||
[FromQuery] string? segmentContainer,
|
||||
[FromQuery] int? segmentLength,
|
||||
[FromQuery] int? minSegments,
|
||||
[FromQuery] string? mediaSourceId,
|
||||
[FromQuery] string? deviceId,
|
||||
[FromQuery] string? audioCodec,
|
||||
[FromQuery] bool? enableAutoStreamCopy,
|
||||
[FromQuery] bool? allowVideoStreamCopy,
|
||||
[FromQuery] bool? allowAudioStreamCopy,
|
||||
[FromQuery] bool? breakOnNonKeyFrames,
|
||||
[FromQuery] int? audioSampleRate,
|
||||
[FromQuery] int? maxAudioBitDepth,
|
||||
[FromQuery] int? audioBitRate,
|
||||
[FromQuery] int? audioChannels,
|
||||
[FromQuery] int? maxAudioChannels,
|
||||
[FromQuery] string? profile,
|
||||
[FromQuery] string? level,
|
||||
[FromQuery] float? framerate,
|
||||
[FromQuery] float? maxFramerate,
|
||||
[FromQuery] bool? copyTimestamps,
|
||||
[FromQuery] long? startTimeTicks,
|
||||
[FromQuery] int? width,
|
||||
[FromQuery] int? height,
|
||||
[FromQuery] int? videoBitRate,
|
||||
[FromQuery] int? subtitleStreamIndex,
|
||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
||||
[FromQuery] int? maxRefFrames,
|
||||
[FromQuery] int? maxVideoBitDepth,
|
||||
[FromQuery] bool? requireAvc,
|
||||
[FromQuery] bool? deInterlace,
|
||||
[FromQuery] bool? requireNonAnamorphic,
|
||||
[FromQuery] int? transcodingMaxAudioChannels,
|
||||
[FromQuery] int? cpuCoreLimit,
|
||||
[FromQuery] string? liveStreamId,
|
||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||
[FromQuery] string? videoCodec,
|
||||
[FromQuery] string? subtitleCodec,
|
||||
[FromQuery] string? transcodingReasons,
|
||||
[FromQuery] int? audioStreamIndex,
|
||||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions)
|
||||
{
|
||||
return GetVideoStream(
|
||||
itemId,
|
||||
container,
|
||||
@static,
|
||||
@params,
|
||||
tag,
|
||||
deviceProfileId,
|
||||
playSessionId,
|
||||
segmentContainer,
|
||||
segmentLength,
|
||||
minSegments,
|
||||
mediaSourceId,
|
||||
deviceId,
|
||||
audioCodec,
|
||||
enableAutoStreamCopy,
|
||||
allowVideoStreamCopy,
|
||||
allowAudioStreamCopy,
|
||||
breakOnNonKeyFrames,
|
||||
audioSampleRate,
|
||||
maxAudioBitDepth,
|
||||
audioBitRate,
|
||||
audioChannels,
|
||||
maxAudioChannels,
|
||||
profile,
|
||||
level,
|
||||
framerate,
|
||||
maxFramerate,
|
||||
copyTimestamps,
|
||||
startTimeTicks,
|
||||
width,
|
||||
height,
|
||||
videoBitRate,
|
||||
subtitleStreamIndex,
|
||||
subtitleMethod,
|
||||
maxRefFrames,
|
||||
maxVideoBitDepth,
|
||||
requireAvc,
|
||||
deInterlace,
|
||||
requireNonAnamorphic,
|
||||
transcodingMaxAudioChannels,
|
||||
cpuCoreLimit,
|
||||
liveStreamId,
|
||||
enableMpegtsM2TsMode,
|
||||
videoCodec,
|
||||
subtitleCodec,
|
||||
transcodingReasons,
|
||||
audioStreamIndex,
|
||||
videoStreamIndex,
|
||||
context,
|
||||
streamOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@ -73,9 +73,9 @@ namespace Jellyfin.Api.Controllers
|
||||
[FromQuery] string? sortOrder,
|
||||
[FromQuery] string? parentId,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
[FromQuery] string? excludeItemTypes,
|
||||
[FromQuery] string? includeItemTypes,
|
||||
[FromQuery] string? mediaTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||
[FromQuery] string? sortBy,
|
||||
[FromQuery] bool? enableUserData,
|
||||
[FromQuery] int? imageTypeLimit,
|
||||
@ -103,19 +103,15 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
IList<BaseItem> items;
|
||||
|
||||
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
|
||||
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
|
||||
var mediaTypesArr = RequestHelpers.Split(mediaTypes, ',', true);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
ExcludeItemTypes = excludeItemTypesArr,
|
||||
IncludeItemTypes = includeItemTypesArr,
|
||||
MediaTypes = mediaTypesArr,
|
||||
ExcludeItemTypes = excludeItemTypes,
|
||||
IncludeItemTypes = includeItemTypes,
|
||||
MediaTypes = mediaTypes,
|
||||
DtoOptions = dtoOptions
|
||||
};
|
||||
|
||||
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr);
|
||||
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
|
||||
|
||||
if (parentItem.IsFolder)
|
||||
{
|
||||
|
@ -207,7 +207,61 @@ namespace Jellyfin.Api.Helpers
|
||||
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
|
||||
}
|
||||
|
||||
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
if (state.VideoStream != null && state.VideoRequest != null)
|
||||
{
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
|
||||
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
|
||||
if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0)
|
||||
{
|
||||
// Force HEVC Main Profile and disable video stream copy.
|
||||
state.OutputVideoCodec = "hevc";
|
||||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
|
||||
EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
|
||||
var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
|
||||
var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
}
|
||||
}
|
||||
|
||||
// Provide Level 5.0 entrance for backward compatibility.
|
||||
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
|
||||
// but in fact it is capable of playing videos up to Level 6.1.
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream.Level.HasValue
|
||||
&& state.VideoStream.Level > 150
|
||||
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
|
||||
&& string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlistCodecsField = new StringBuilder();
|
||||
AppendPlaylistCodecsField(playlistCodecsField, state);
|
||||
|
||||
// Force the video level to 5.0.
|
||||
var originalLevel = state.VideoStream.Level;
|
||||
state.VideoStream.Level = 150;
|
||||
var newPlaylistCodecsField = new StringBuilder();
|
||||
AppendPlaylistCodecsField(newPlaylistCodecsField, state);
|
||||
|
||||
// Restore the video level.
|
||||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
builder.Append(newPlaylist);
|
||||
}
|
||||
}
|
||||
|
||||
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
|
||||
{
|
||||
@ -217,40 +271,77 @@ namespace Jellyfin.Api.Helpers
|
||||
var variation = GetBitrateVariation(totalBitrate);
|
||||
|
||||
var newBitrate = totalBitrate - variation;
|
||||
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||
|
||||
variation *= 2;
|
||||
newBitrate = totalBitrate - variation;
|
||||
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
|
||||
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
|
||||
}
|
||||
|
||||
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
|
||||
}
|
||||
|
||||
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
||||
private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
|
||||
{
|
||||
builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
||||
var playlistBuilder = new StringBuilder();
|
||||
playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
|
||||
.Append(",AVERAGE-BANDWIDTH=")
|
||||
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
AppendPlaylistCodecsField(builder, state);
|
||||
AppendPlaylistVideoRangeField(playlistBuilder, state);
|
||||
|
||||
AppendPlaylistResolutionField(builder, state);
|
||||
AppendPlaylistCodecsField(playlistBuilder, state);
|
||||
|
||||
AppendPlaylistFramerateField(builder, state);
|
||||
AppendPlaylistResolutionField(playlistBuilder, state);
|
||||
|
||||
AppendPlaylistFramerateField(playlistBuilder, state);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subtitleGroup))
|
||||
{
|
||||
builder.Append(",SUBTITLES=\"")
|
||||
playlistBuilder.Append(",SUBTITLES=\"")
|
||||
.Append(subtitleGroup)
|
||||
.Append('"');
|
||||
}
|
||||
|
||||
builder.Append(Environment.NewLine);
|
||||
builder.AppendLine(url);
|
||||
playlistBuilder.Append(Environment.NewLine);
|
||||
playlistBuilder.AppendLine(url);
|
||||
builder.Append(playlistBuilder);
|
||||
|
||||
return playlistBuilder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends a VIDEO-RANGE field containing the range of the output video stream.
|
||||
/// </summary>
|
||||
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
|
||||
/// <param name="builder">StringBuilder to append the field to.</param>
|
||||
/// <param name="state">StreamState of the current stream.</param>
|
||||
private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
|
||||
{
|
||||
if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
|
||||
{
|
||||
var videoRange = state.VideoStream.VideoRange;
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.Append(",VIDEO-RANGE=SDR");
|
||||
}
|
||||
|
||||
if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.Append(",VIDEO-RANGE=PQ");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Currently we only encode to SDR.
|
||||
builder.Append(",VIDEO-RANGE=SDR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -419,15 +510,27 @@ namespace Jellyfin.Api.Helpers
|
||||
/// <returns>H.26X level of the output video stream.</returns>
|
||||
private int? GetOutputVideoCodecLevel(StreamState state)
|
||||
{
|
||||
string? levelString;
|
||||
string levelString = string.Empty;
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& state.VideoStream != null
|
||||
&& state.VideoStream.Level.HasValue)
|
||||
{
|
||||
levelString = state.VideoStream?.Level.ToString();
|
||||
levelString = state.VideoStream.Level.ToString() ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
|
||||
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
|
||||
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
|
||||
}
|
||||
}
|
||||
|
||||
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
|
||||
@ -438,6 +541,38 @@ namespace Jellyfin.Api.Helpers
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the H.26X profile of the output video stream.
|
||||
/// </summary>
|
||||
/// <param name="state">StreamState of the current stream.</param>
|
||||
/// <param name="codec">Video codec.</param>
|
||||
/// <returns>H.26X profile of the output video stream.</returns>
|
||||
private string GetOutputVideoCodecProfile(StreamState state, string codec)
|
||||
{
|
||||
string profileString = string.Empty;
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||
&& !string.IsNullOrEmpty(state.VideoStream.Profile))
|
||||
{
|
||||
profileString = state.VideoStream.Profile;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profileString = profileString ?? "high";
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profileString = profileString ?? "main";
|
||||
}
|
||||
}
|
||||
|
||||
return profileString;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
|
||||
/// </summary>
|
||||
@ -468,6 +603,16 @@ namespace Jellyfin.Api.Helpers
|
||||
return HlsCodecStringHelpers.GetEAC3String();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HlsCodecStringHelpers.GetFLACString();
|
||||
}
|
||||
|
||||
if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return HlsCodecStringHelpers.GetALACString();
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@ -492,15 +637,14 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string? profile = state.GetRequestedProfiles("h264").FirstOrDefault();
|
||||
string profile = GetOutputVideoCodecProfile(state, "h264");
|
||||
return HlsCodecStringHelpers.GetH264String(profile, level);
|
||||
}
|
||||
|
||||
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string? profile = state.GetRequestedProfiles("h265").FirstOrDefault();
|
||||
|
||||
string profile = GetOutputVideoCodecProfile(state, "hevc");
|
||||
return HlsCodecStringHelpers.GetH265String(profile, level);
|
||||
}
|
||||
|
||||
@ -544,12 +688,30 @@ namespace Jellyfin.Api.Helpers
|
||||
return variation;
|
||||
}
|
||||
|
||||
private string ReplaceBitrate(string url, int oldValue, int newValue)
|
||||
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
|
||||
{
|
||||
return url.Replace(
|
||||
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
|
||||
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
|
||||
{
|
||||
string profileStr = codec + "-profile=";
|
||||
return url.Replace(
|
||||
profileStr + oldValue,
|
||||
profileStr + newValue,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
|
||||
{
|
||||
var oldPlaylist = playlist.ToString();
|
||||
return oldPlaylist.Replace(
|
||||
oldValue.ToString(),
|
||||
newValue.ToString(),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers
|
||||
/// </summary>
|
||||
public static class HlsCodecStringHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Codec name for MP3.
|
||||
/// </summary>
|
||||
public const string MP3 = "mp4a.40.34";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for AC-3.
|
||||
/// </summary>
|
||||
public const string AC3 = "mp4a.a5";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for E-AC-3.
|
||||
/// </summary>
|
||||
public const string EAC3 = "mp4a.a6";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "fLaC";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
/// </summary>
|
||||
public const string ALAC = "alac";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
/// </summary>
|
||||
/// <returns>MP3 codec string.</returns>
|
||||
public static string GetMP3String()
|
||||
{
|
||||
return "mp4a.40.34";
|
||||
return MP3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AC-3 codec string.
|
||||
/// </summary>
|
||||
/// <returns>AC-3 codec string.</returns>
|
||||
public static string GetAC3String()
|
||||
{
|
||||
return AC3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an E-AC-3 codec string.
|
||||
/// </summary>
|
||||
/// <returns>E-AC-3 codec string.</returns>
|
||||
public static string GetEAC3String()
|
||||
{
|
||||
return EAC3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an FLAC codec string.
|
||||
/// </summary>
|
||||
/// <returns>FLAC codec string.</returns>
|
||||
public static string GetFLACString()
|
||||
{
|
||||
return FLAC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an ALAC codec string.
|
||||
/// </summary>
|
||||
/// <returns>ALAC codec string.</returns>
|
||||
public static string GetALACString()
|
||||
{
|
||||
return ALAC;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a H.264 codec string.
|
||||
/// </summary>
|
||||
@ -85,41 +146,24 @@ namespace Jellyfin.Api.Helpers
|
||||
// The h265 syntax is a bit of a mystery at the time this comment was written.
|
||||
// This is what I've found through various sources:
|
||||
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
|
||||
StringBuilder result = new StringBuilder("hev1", 16);
|
||||
StringBuilder result = new StringBuilder("hvc1", 16);
|
||||
|
||||
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result.Append(".2.6");
|
||||
result.Append(".2.4");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to main if profile is invalid
|
||||
result.Append(".1.6");
|
||||
result.Append(".1.4");
|
||||
}
|
||||
|
||||
result.Append(".L")
|
||||
.Append(level * 3)
|
||||
.Append(level)
|
||||
.Append(".B0");
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an AC-3 codec string.
|
||||
/// </summary>
|
||||
/// <returns>AC-3 codec string.</returns>
|
||||
public static string GetAC3String()
|
||||
{
|
||||
return "mp4a.a5";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an E-AC-3 codec string.
|
||||
/// </summary>
|
||||
/// <returns>E-AC-3 codec string.</returns>
|
||||
public static string GetEAC3String()
|
||||
{
|
||||
return "mp4a.a6";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -74,25 +77,65 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the #EXT-X-MAP string.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The output path of the file.</param>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <param name="isOsDepends">Get a normal string or depends on OS.</param>
|
||||
/// <returns>The string text of #EXT-X-MAP.</returns>
|
||||
public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
|
||||
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
|
||||
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
|
||||
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
|
||||
|
||||
// on Linux/Unix
|
||||
// #EXT-X-MAP:URI="prefix-1.mp4"
|
||||
var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
|
||||
if (!isOsDepends)
|
||||
{
|
||||
return fmp4InitFileName;
|
||||
}
|
||||
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
{
|
||||
// on Windows
|
||||
// #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
|
||||
fmp4InitFileName = outputPrefix + "-1" + outputExtension;
|
||||
}
|
||||
|
||||
return fmp4InitFileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hls playlist text.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the playlist file.</param>
|
||||
/// <param name="segmentLength">The segment length.</param>
|
||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||
/// <returns>The playlist text as a string.</returns>
|
||||
public static string GetLivePlaylistText(string path, int segmentLength)
|
||||
public static string GetLivePlaylistText(string path, StreamState state)
|
||||
{
|
||||
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream);
|
||||
|
||||
var text = reader.ReadToEnd();
|
||||
|
||||
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
|
||||
var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
|
||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
|
||||
var baseUrlParam = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"hls/{0}/",
|
||||
Path.GetFileNameWithoutExtension(path));
|
||||
var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
|
||||
|
||||
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
// text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
|
||||
// Replace fMP4 init file URI.
|
||||
text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers
|
||||
return session;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get Guid array from string.
|
||||
/// </summary>
|
||||
/// <param name="value">String value.</param>
|
||||
/// <returns>Guid array.</returns>
|
||||
internal static Guid[] GetGuids(string? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
return Split(value, ',', true)
|
||||
.Select(i => new Guid(i))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item fields.
|
||||
/// </summary>
|
||||
/// <param name="fields">The fields string.</param>
|
||||
/// <returns>IEnumerable{ItemFields}.</returns>
|
||||
internal static ItemFields[] GetItemFields(string? fields)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fields))
|
||||
{
|
||||
return Array.Empty<ItemFields>();
|
||||
}
|
||||
|
||||
return Split(fields, ',', true)
|
||||
.Select(v =>
|
||||
{
|
||||
if (Enum.TryParse(v, true, out ItemFields value))
|
||||
{
|
||||
return (ItemFields?)value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}).Where(i => i.HasValue)
|
||||
.Select(i => i!.Value)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
internal static QueryResult<BaseItemDto> CreateQueryResult(
|
||||
QueryResult<(BaseItem, ItemCounts)> result,
|
||||
DtoOptions dtoOptions,
|
||||
|
@ -169,7 +169,9 @@ namespace Jellyfin.Api.Helpers
|
||||
state.DirectStreamProvider = liveStreamInfo.Item2;
|
||||
}
|
||||
|
||||
encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
|
||||
var encodingOptions = serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
|
||||
|
||||
string? containerInternal = Path.GetExtension(state.RequestedUrl);
|
||||
|
||||
@ -187,7 +189,7 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
|
||||
|
||||
state.OutputAudioCodec = streamingRequest.AudioCodec;
|
||||
|
||||
@ -200,20 +202,41 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
encodingHelper.TryStreamCopy(state);
|
||||
|
||||
if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
|
||||
{
|
||||
var resolution = ResolutionNormalizer.Normalize(
|
||||
state.VideoStream?.BitRate,
|
||||
state.VideoStream?.Width,
|
||||
state.VideoStream?.Height,
|
||||
state.OutputVideoBitrate.Value,
|
||||
state.VideoStream?.Codec,
|
||||
state.OutputVideoCodec,
|
||||
state.VideoRequest.MaxWidth,
|
||||
state.VideoRequest.MaxHeight);
|
||||
var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
|
||||
&& !state.VideoRequest.Height.HasValue
|
||||
&& !state.VideoRequest.MaxWidth.HasValue
|
||||
&& !state.VideoRequest.MaxHeight.HasValue;
|
||||
|
||||
state.VideoRequest.MaxWidth = resolution.MaxWidth;
|
||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||
if (isVideoResolutionNotRequested
|
||||
&& state.VideoRequest.VideoBitRate.HasValue
|
||||
&& state.VideoStream.BitRate.HasValue
|
||||
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
|
||||
{
|
||||
// Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
|
||||
// and the requested video bitrate is higher than source video bitrate.
|
||||
if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
|
||||
{
|
||||
state.VideoRequest.MaxWidth = state.VideoStream?.Width;
|
||||
state.VideoRequest.MaxHeight = state.VideoStream?.Height;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var resolution = ResolutionNormalizer.Normalize(
|
||||
state.VideoStream?.BitRate,
|
||||
state.VideoStream?.Width,
|
||||
state.VideoStream?.Height,
|
||||
state.OutputVideoBitrate.Value,
|
||||
state.VideoStream?.Codec,
|
||||
state.OutputVideoCodec,
|
||||
state.VideoRequest.MaxWidth,
|
||||
state.VideoRequest.MaxHeight);
|
||||
|
||||
state.VideoRequest.MaxWidth = resolution.MaxWidth;
|
||||
state.VideoRequest.MaxHeight = resolution.MaxHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ namespace Jellyfin.Api.Helpers
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably
|
||||
// Progressive streams can stop on their own reliably.
|
||||
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
@ -241,7 +241,7 @@ namespace Jellyfin.Api.Helpers
|
||||
lock (_activeTranscodingJobs)
|
||||
{
|
||||
// This is really only needed for HLS.
|
||||
// Progressive streams can stop on their own reliably
|
||||
// Progressive streams can stop on their own reliably.
|
||||
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
|
||||
}
|
||||
|
||||
@ -304,10 +304,10 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
process!.StandardInput.WriteLine("q");
|
||||
|
||||
// Need to wait because killing is asynchronous
|
||||
// Need to wait because killing is asynchronous.
|
||||
if (!process.WaitForExit(5000))
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
|
||||
_logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
|
||||
process.Kill();
|
||||
}
|
||||
}
|
||||
@ -470,11 +470,11 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the FFMPEG.
|
||||
/// Starts FFmpeg.
|
||||
/// </summary>
|
||||
/// <param name="state">The state.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
|
||||
/// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
|
||||
/// <param name="request">The <see cref="HttpRequest"/>.</param>
|
||||
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
|
||||
/// <param name="cancellationTokenSource">The cancellation token source.</param>
|
||||
@ -501,13 +501,13 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw new ArgumentException("User does not have access to video transcoding");
|
||||
throw new ArgumentException("User does not have access to video transcoding.");
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
|
||||
{
|
||||
throw new ArgumentException("FFMPEG path not set.");
|
||||
throw new ArgumentException("FFmpeg path not set.");
|
||||
}
|
||||
|
||||
var process = new Process
|
||||
@ -544,18 +544,20 @@ namespace Jellyfin.Api.Helpers
|
||||
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
|
||||
_logger.LogInformation(commandLineLogMessage);
|
||||
|
||||
var logFilePrefix = "ffmpeg-transcode";
|
||||
var logFilePrefix = "FFmpeg.Transcode-";
|
||||
if (state.VideoRequest != null
|
||||
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
|
||||
? "ffmpeg-remux"
|
||||
: "ffmpeg-directstream";
|
||||
? "FFmpeg.Remux-"
|
||||
: "FFmpeg.DirectStream-";
|
||||
}
|
||||
|
||||
var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
|
||||
var logFilePath = Path.Combine(
|
||||
_serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
|
||||
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
|
||||
|
||||
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
|
||||
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
||||
|
||||
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
|
||||
@ -569,17 +571,17 @@ namespace Jellyfin.Api.Helpers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
_logger.LogError(ex, "Error starting FFmpeg");
|
||||
|
||||
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
_logger.LogDebug("Launched ffmpeg process");
|
||||
_logger.LogDebug("Launched FFmpeg process");
|
||||
state.TranscodingJob = transcodingJob;
|
||||
|
||||
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
|
||||
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
|
||||
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
|
||||
|
||||
// Wait for the file to exist before proceeding
|
||||
@ -748,11 +750,11 @@ namespace Jellyfin.Api.Helpers
|
||||
|
||||
if (process.ExitCode == 0)
|
||||
{
|
||||
_logger.LogInformation("FFMpeg exited with code 0");
|
||||
_logger.LogInformation("FFmpeg exited with code 0");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
|
||||
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
|
||||
}
|
||||
|
||||
process.Dispose();
|
||||
@ -771,8 +773,9 @@ namespace Jellyfin.Api.Helpers
|
||||
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
|
||||
cancellationTokenSource.Token)
|
||||
.ConfigureAwait(false);
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
_encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
|
||||
_encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
|
||||
|
||||
if (state.VideoRequest != null)
|
||||
{
|
||||
|
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);
|
||||
}
|
||||
}
|
||||
}
|
90
Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
Normal file
90
Jellyfin.Api/ModelBinders/PipeDelimitedArrayModelBinder.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.ModelBinders
|
||||
{
|
||||
/// <summary>
|
||||
/// Comma delimited array model binder.
|
||||
/// Returns an empty array of specified type if there is no query parameter.
|
||||
/// </summary>
|
||||
public class PipeDelimitedArrayModelBinder : IModelBinder
|
||||
{
|
||||
private readonly ILogger<PipeDelimitedArrayModelBinder> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PipeDelimitedArrayModelBinder"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{PipeDelimitedArrayModelBinder}"/> interface.</param>
|
||||
public PipeDelimitedArrayModelBinder(ILogger<PipeDelimitedArrayModelBinder> 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 > 1)
|
||||
{
|
||||
var typedValues = GetParsedResult(valueProviderResult.Values, elementType, converter);
|
||||
bindingContext.Result = ModelBindingResult.Success(typedValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
var value = valueProviderResult.FirstValue;
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
var splitValues = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
var typedValues = GetParsedResult(splitValues, elementType, converter);
|
||||
bindingContext.Result = ModelBindingResult.Success(typedValues);
|
||||
}
|
||||
else
|
||||
{
|
||||
var emptyResult = Array.CreateInstance(elementType, 0);
|
||||
bindingContext.Result = ModelBindingResult.Success(emptyResult);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Array GetParsedResult(IReadOnlyList<string> values, Type elementType, TypeConverter converter)
|
||||
{
|
||||
var parsedValues = new object?[values.Count];
|
||||
var convertedCount = 0;
|
||||
for (var i = 0; i < values.Count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
parsedValues[i] = converter.ConvertFromString(values[i].Trim());
|
||||
convertedCount++;
|
||||
}
|
||||
catch (FormatException e)
|
||||
{
|
||||
_logger.LogWarning(e, "Error converting value.");
|
||||
}
|
||||
}
|
||||
|
||||
var typedValues = Array.CreateInstance(elementType, convertedCount);
|
||||
var typedValueIndex = 0;
|
||||
for (var i = 0; i < parsedValues.Length; i++)
|
||||
{
|
||||
if (parsedValues[i] != null)
|
||||
{
|
||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||
typedValueIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return typedValues;
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos
|
||||
/// <summary>
|
||||
/// Gets or sets the channels to return guide information for.
|
||||
/// </summary>
|
||||
public string? ChannelIds { get; set; }
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets optional. Filter by user id.
|
||||
@ -115,12 +116,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos
|
||||
/// <summary>
|
||||
/// Gets or sets the genres to return guide information for.
|
||||
/// </summary>
|
||||
public string? Genres { get; set; }
|
||||
[JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the genre ids to return guide information for.
|
||||
/// </summary>
|
||||
public string? GenreIds { get; set; }
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets include image information in output.
|
||||
|
@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Common.Json.Converters;
|
||||
|
||||
namespace Jellyfin.Api.Models.PlaylistDtos
|
||||
{
|
||||
@ -15,7 +18,8 @@ namespace Jellyfin.Api.Models.PlaylistDtos
|
||||
/// <summary>
|
||||
/// Gets or sets item ids to add to the playlist.
|
||||
/// </summary>
|
||||
public string? Ids { get; set; }
|
||||
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
|
||||
public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
|
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);
|
||||
}
|
||||
}
|
||||
}
|
221
Jellyfin.Networking/Configuration/NetworkConfiguration.cs
Normal file
221
Jellyfin.Networking/Configuration/NetworkConfiguration.cs
Normal file
@ -0,0 +1,221 @@
|
||||
#pragma warning disable CA1819 // Properties should not return arrays
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace Jellyfin.Networking.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="NetworkConfiguration" />.
|
||||
/// </summary>
|
||||
public class NetworkConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The default value for <see cref="HttpServerPortNumber"/>.
|
||||
/// </summary>
|
||||
public const int DefaultHttpPort = 8096;
|
||||
|
||||
/// <summary>
|
||||
/// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
|
||||
/// </summary>
|
||||
public const int DefaultHttpsPort = 8920;
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the server should force connections over HTTPS.
|
||||
/// </summary>
|
||||
public bool RequireHttps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
|
||||
/// </summary>
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
set
|
||||
{
|
||||
// Normalize the start of the string
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// If baseUrl is empty, set an empty prefix string
|
||||
_baseUrl = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value[0] != '/')
|
||||
{
|
||||
// If baseUrl was not configured with a leading slash, append one for consistency
|
||||
value = "/" + value;
|
||||
}
|
||||
|
||||
// Normalize the end of the string
|
||||
if (value[^1] == '/')
|
||||
{
|
||||
// If baseUrl was configured with a trailing slash, remove it for consistency
|
||||
value = value.Remove(value.Length - 1);
|
||||
}
|
||||
|
||||
_baseUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public HTTPS port.
|
||||
/// </summary>
|
||||
/// <value>The public HTTPS port.</value>
|
||||
public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP server port number.
|
||||
/// </summary>
|
||||
/// <value>The HTTP server port number.</value>
|
||||
public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTPS server port number.
|
||||
/// </summary>
|
||||
/// <value>The HTTPS server port number.</value>
|
||||
public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use HTTPS.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
|
||||
/// provided for <see cref="ServerConfiguration.CertificatePath"/> and <see cref="ServerConfiguration.CertificatePassword"/>.
|
||||
/// </remarks>
|
||||
public bool EnableHttps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public mapped port.
|
||||
/// </summary>
|
||||
/// <value>The public mapped port.</value>
|
||||
public int PublicPort { get; set; } = DefaultHttpPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
|
||||
/// </summary>
|
||||
public bool UPnPCreateHttpPortMap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the UDPPortRange.
|
||||
/// </summary>
|
||||
public string UDPPortRange { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets IPV6 capability.
|
||||
/// </summary>
|
||||
public bool EnableIPV6 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether gets or sets IPV4 capability.
|
||||
/// </summary>
|
||||
public bool EnableIPV4 { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detailed SSDP logs are sent to the console/log.
|
||||
/// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to have any effect.
|
||||
/// </summary>
|
||||
public bool EnableSSDPTracing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SSDPTracingFilter
|
||||
/// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
|
||||
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public string SSDPTracingFilter { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times SSDP UDP messages are sent.
|
||||
/// </summary>
|
||||
public int UDPSendCount { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay between each groups of SSDP messages (in ms).
|
||||
/// </summary>
|
||||
public int UDPSendDelay { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
|
||||
/// </summary>
|
||||
public bool IgnoreVirtualInterfaces { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
|
||||
/// </summary>
|
||||
public string VirtualInterfaceNames { get; set; } = "vEthernet*";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
|
||||
/// </summary>
|
||||
public int GatewayMonitorPeriod { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether multi-socket binding is available.
|
||||
/// </summary>
|
||||
public bool EnableMultiSocketBinding { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
|
||||
/// Depending on the address range implemented ULA ranges might not be used.
|
||||
/// </summary>
|
||||
public bool TrustAllIP6Interfaces { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ports that HDHomerun uses.
|
||||
/// </summary>
|
||||
public string HDHomerunPortRange { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the PublishedServerUriBySubnet
|
||||
/// Gets or sets PublishedServerUri to advertise for specific subnets.
|
||||
/// </summary>
|
||||
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Autodiscovery tracing is enabled.
|
||||
/// </summary>
|
||||
public bool AutoDiscoveryTracing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Autodiscovery is enabled.
|
||||
/// </summary>
|
||||
public bool AutoDiscovery { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
|
||||
/// </summary>
|
||||
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
|
||||
/// </summary>
|
||||
public bool IsRemoteIPFilterBlacklist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable automatic port forwarding.
|
||||
/// </summary>
|
||||
public bool EnableUPnP { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether access outside of the LAN is permitted.
|
||||
/// </summary>
|
||||
public bool EnableRemoteAccess { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subnets that are deemed to make up the LAN.
|
||||
/// </summary>
|
||||
public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
|
||||
/// </summary>
|
||||
public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the known proxies.
|
||||
/// </summary>
|
||||
public string[] KnownProxies { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Networking.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="NetworkConfigurationExtensions" />.
|
||||
/// </summary>
|
||||
public static class NetworkConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the network configuration.
|
||||
/// </summary>
|
||||
/// <param name="config">The <see cref="IConfigurationManager"/>.</param>
|
||||
/// <returns>The <see cref="NetworkConfiguration"/>.</returns>
|
||||
public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
|
||||
{
|
||||
return config.GetConfiguration<NetworkConfiguration>("network");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Jellyfin.Networking.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="NetworkConfigurationFactory" />.
|
||||
/// </summary>
|
||||
public class NetworkConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// The GetConfigurations.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns>
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "network",
|
||||
ConfigurationType = typeof(NetworkConfiguration)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
30
Jellyfin.Networking/Jellyfin.Networking.csproj
Normal file
30
Jellyfin.Networking/Jellyfin.Networking.csproj
Normal file
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
234
Jellyfin.Networking/Manager/INetworkManager.cs
Normal file
234
Jellyfin.Networking/Manager/INetworkManager.cs
Normal file
@ -0,0 +1,234 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for the NetworkManager class.
|
||||
/// </summary>
|
||||
public interface INetworkManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Event triggered on network changes.
|
||||
/// </summary>
|
||||
event EventHandler NetworkChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the published server urls list.
|
||||
/// </summary>
|
||||
Dictionary<IPNetAddress, string> PublishedServerUrls { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether is all IPv6 interfaces are trusted as internal.
|
||||
/// </summary>
|
||||
bool TrustAllIP6Interfaces { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote address filter.
|
||||
/// </summary>
|
||||
Collection<IPObject> RemoteAddressFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether iP6 is enabled.
|
||||
/// </summary>
|
||||
bool IsIP6Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether iP4 is enabled.
|
||||
/// </summary>
|
||||
bool IsIP4Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the list of interfaces to use for Kestrel.
|
||||
/// </summary>
|
||||
/// <returns>A Collection{IPObject} object containing all the interfaces to bind.
|
||||
/// If all the interfaces are specified, and none are excluded, it returns zero items
|
||||
/// to represent any address.</returns>
|
||||
/// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param>
|
||||
Collection<IPObject> GetAllBindInterfaces(bool individualInterfaces = false);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a collection containing the loopback interfaces.
|
||||
/// </summary>
|
||||
/// <returns>Collection{IPObject}.</returns>
|
||||
Collection<IPObject> GetLoopbacks();
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
|
||||
/// If no bind addresses are specified, an internal interface address is selected.
|
||||
/// The priority of selection is as follows:-
|
||||
///
|
||||
/// The value contained in the startup parameter --published-server-url.
|
||||
///
|
||||
/// If the user specified custom subnet overrides, the correct subnet for the source address.
|
||||
///
|
||||
/// If the user specified bind interfaces to use:-
|
||||
/// The bind interface that contains the source subnet.
|
||||
/// The first bind interface specified that suits best first the source's endpoint. eg. external or internal.
|
||||
///
|
||||
/// If the source is from a public subnet address range and the user hasn't specified any bind addresses:-
|
||||
/// The first public interface that isn't a loopback and contains the source subnet.
|
||||
/// The first public interface that isn't a loopback. Priority is given to interfaces with gateways.
|
||||
/// An internal interface if there are no public ip addresses.
|
||||
///
|
||||
/// If the source is from a private subnet address range and the user hasn't specified any bind addresses:-
|
||||
/// The first private interface that contains the source subnet.
|
||||
/// The first private interface that isn't a loopback. Priority is given to interfaces with gateways.
|
||||
///
|
||||
/// If no interfaces meet any of these criteria, then a loopback address is returned.
|
||||
///
|
||||
/// Interface that have been specifically excluded from binding are not used in any of the calculations.
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the request.</param>
|
||||
/// <param name="port">Optional port returned, if it's part of an override.</param>
|
||||
/// <returns>IP Address to use, or loopback address if all else fails.</returns>
|
||||
string GetBindInterface(IPObject source, out int? port);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
|
||||
/// If no bind addresses are specified, an internal interface address is selected.
|
||||
/// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the request.</param>
|
||||
/// <param name="port">Optional port returned, if it's part of an override.</param>
|
||||
/// <returns>IP Address to use, or loopback address if all else fails.</returns>
|
||||
string GetBindInterface(HttpRequest source, out int? port);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
|
||||
/// If no bind addresses are specified, an internal interface address is selected.
|
||||
/// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">IP address of the request.</param>
|
||||
/// <param name="port">Optional port returned, if it's part of an override.</param>
|
||||
/// <returns>IP Address to use, or loopback address if all else fails.</returns>
|
||||
string GetBindInterface(IPAddress source, out int? port);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the bind address to use in system url's. (Server Discovery, PlayTo, LiveTV, SystemInfo)
|
||||
/// If no bind addresses are specified, an internal interface address is selected.
|
||||
/// (See <see cref="GetBindInterface(IPObject, out int?)"/>.
|
||||
/// </summary>
|
||||
/// <param name="source">Source of the request.</param>
|
||||
/// <param name="port">Optional port returned, if it's part of an override.</param>
|
||||
/// <returns>IP Address to use, or loopback address if all else fails.</returns>
|
||||
string GetBindInterface(string source, out int? port);
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the ip address is specifically excluded in LocalNetworkAddresses.
|
||||
/// </summary>
|
||||
/// <param name="address">IP address to check.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
bool IsExcludedInterface(IPAddress address);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of all the MAC addresses associated with active interfaces.
|
||||
/// </summary>
|
||||
/// <returns>List of MAC addresses.</returns>
|
||||
IReadOnlyCollection<PhysicalAddress> GetMacAddresses();
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the IP Address provided matches an interface that has a gateway.
|
||||
/// </summary>
|
||||
/// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
|
||||
/// <returns>Result of the check.</returns>
|
||||
bool IsGatewayInterface(IPObject? addressObj);
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if the IP Address provided matches an interface that has a gateway.
|
||||
/// </summary>
|
||||
/// <param name="addressObj">IP to check. Can be an IPAddress or an IPObject.</param>
|
||||
/// <returns>Result of the check.</returns>
|
||||
bool IsGatewayInterface(IPAddress? addressObj);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the address is a private address.
|
||||
/// The config option TrustIP6Interfaces overrides this functions behaviour.
|
||||
/// </summary>
|
||||
/// <param name="address">Address to check.</param>
|
||||
/// <returns>True or False.</returns>
|
||||
bool IsPrivateAddressRange(IPObject address);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the address is part of the user defined LAN.
|
||||
/// The config option TrustIP6Interfaces overrides this functions behaviour.
|
||||
/// </summary>
|
||||
/// <param name="address">IP to check.</param>
|
||||
/// <returns>True if endpoint is within the LAN range.</returns>
|
||||
bool IsInLocalNetwork(string address);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the address is part of the user defined LAN.
|
||||
/// The config option TrustIP6Interfaces overrides this functions behaviour.
|
||||
/// </summary>
|
||||
/// <param name="address">IP to check.</param>
|
||||
/// <returns>True if endpoint is within the LAN range.</returns>
|
||||
bool IsInLocalNetwork(IPObject address);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the address is part of the user defined LAN.
|
||||
/// The config option TrustIP6Interfaces overrides this functions behaviour.
|
||||
/// </summary>
|
||||
/// <param name="address">IP to check.</param>
|
||||
/// <returns>True if endpoint is within the LAN range.</returns>
|
||||
bool IsInLocalNetwork(IPAddress address);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to convert the token to an IP address, permitting for interface descriptions and indexes.
|
||||
/// eg. "eth1", or "TP-LINK Wireless USB Adapter".
|
||||
/// </summary>
|
||||
/// <param name="token">Token to parse.</param>
|
||||
/// <param name="result">Resultant object's ip addresses, if successful.</param>
|
||||
/// <returns>Success of the operation.</returns>
|
||||
bool TryParseInterface(string token, out Collection<IPObject>? result);
|
||||
|
||||
/// <summary>
|
||||
/// Parses an array of strings into a Collection{IPObject}.
|
||||
/// </summary>
|
||||
/// <param name="values">Values to parse.</param>
|
||||
/// <param name="bracketed">When true, only include values in []. When false, ignore bracketed values.</param>
|
||||
/// <returns>IPCollection object containing the value strings.</returns>
|
||||
Collection<IPObject> CreateIPCollection(string[] values, bool bracketed = false);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the internal Bind interface addresses.
|
||||
/// </summary>
|
||||
/// <returns>An internal list of interfaces addresses.</returns>
|
||||
Collection<IPObject> GetInternalBindAddresses();
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if an IP address is still a valid interface address.
|
||||
/// </summary>
|
||||
/// <param name="address">IP address to check.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
bool IsValidInterfaceAddress(IPAddress address);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the IP address is in the excluded list.
|
||||
/// </summary>
|
||||
/// <param name="ip">IP to check.</param>
|
||||
/// <returns>True if excluded.</returns>
|
||||
bool IsExcluded(IPAddress ip);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the IP address is in the excluded list.
|
||||
/// </summary>
|
||||
/// <param name="ip">IP to check.</param>
|
||||
/// <returns>True if excluded.</returns>
|
||||
bool IsExcluded(EndPoint ip);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the filtered LAN ip addresses.
|
||||
/// </summary>
|
||||
/// <param name="filter">Optional filter for the list.</param>
|
||||
/// <returns>Returns a filtered list of LAN addresses.</returns>
|
||||
Collection<IPObject> GetFilteredLANSubnets(Collection<IPObject>? filter = null);
|
||||
}
|
||||
}
|
1319
Jellyfin.Networking/Manager/NetworkManager.cs
Normal file
1319
Jellyfin.Networking/Manager/NetworkManager.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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.Server.Extensions;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Jellyfin.Server.Middleware;
|
||||
@ -66,10 +63,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());
|
||||
|
||||
@ -77,6 +80,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());
|
||||
|
||||
@ -164,9 +169,6 @@ namespace Jellyfin.Server
|
||||
endpoints.MapHealthChecks("/health");
|
||||
});
|
||||
});
|
||||
|
||||
// Add type descriptor for legacy datetime parsing.
|
||||
TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@ namespace MediaBrowser.Common.Json.Converters
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// TODO log when upgraded to .Net5
|
||||
// TODO log when upgraded to .Net6
|
||||
// https://github.com/dotnet/runtime/issues/42975
|
||||
// _logger.LogWarning(e, "Error converting value.");
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Common.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns an ISO8601 formatted datetime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Used for legacy compatibility.
|
||||
/// </remarks>
|
||||
public class JsonDateTimeIso8601Converter : JsonConverter<DateTime>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> reader.GetDateTime();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString("O", CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Common.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert Pipe delimited string to array of type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to convert to.</typeparam>
|
||||
public class JsonPipeDelimitedArrayConverter<T> : JsonConverter<T[]>
|
||||
{
|
||||
private readonly TypeConverter _typeConverter;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonPipeDelimitedArrayConverter{T}"/> class.
|
||||
/// </summary>
|
||||
public JsonPipeDelimitedArrayConverter()
|
||||
{
|
||||
_typeConverter = TypeDescriptor.GetConverter(typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var stringEntries = reader.GetString()?.Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (stringEntries == null || stringEntries.Length == 0)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
var parsedValues = new object[stringEntries.Length];
|
||||
var convertedCount = 0;
|
||||
for (var i = 0; i < stringEntries.Length; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
parsedValues[i] = _typeConverter.ConvertFrom(stringEntries[i].Trim());
|
||||
convertedCount++;
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// TODO log when upgraded to .Net6
|
||||
// https://github.com/dotnet/runtime/issues/42975
|
||||
// _logger.LogWarning(e, "Error converting value.");
|
||||
}
|
||||
}
|
||||
|
||||
var typedValues = new T[convertedCount];
|
||||
var typedValueIndex = 0;
|
||||
for (var i = 0; i < stringEntries.Length; i++)
|
||||
{
|
||||
if (parsedValues[i] != null)
|
||||
{
|
||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||
typedValueIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
return typedValues;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T[]>(ref reader, options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||
{
|
||||
JsonSerializer.Serialize(writer, value, options);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Common.Json.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Json Pipe delimited array converter factory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This must be applied as an attribute, adding to the JsonConverter list causes stack overflow.
|
||||
/// </remarks>
|
||||
public class JsonPipeDelimitedArrayConverterFactory : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var structType = typeToConvert.GetElementType() ?? typeToConvert.GenericTypeArguments[0];
|
||||
return (JsonConverter)Activator.CreateInstance(typeof(JsonPipeDelimitedArrayConverter<>).MakeGenericType(structType));
|
||||
}
|
||||
}
|
||||
}
|
@ -42,6 +42,7 @@ namespace MediaBrowser.Common.Json
|
||||
options.Converters.Add(new JsonGuidConverter());
|
||||
options.Converters.Add(new JsonStringEnumConverter());
|
||||
options.Converters.Add(new JsonNullableStructConverterFactory());
|
||||
options.Converters.Add(new JsonDateTimeIso8601Converter());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
|
||||
</ItemGroup>
|
||||
|
||||
|
445
MediaBrowser.Common/Net/IPHost.cs
Normal file
445
MediaBrowser.Common/Net/IPHost.cs
Normal file
@ -0,0 +1,445 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Object that holds a host name.
|
||||
/// </summary>
|
||||
public class IPHost : IPObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets timeout value before resolve required, in minutes.
|
||||
/// </summary>
|
||||
public const int Timeout = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an IPHost that has no value.
|
||||
/// </summary>
|
||||
public static readonly IPHost None = new IPHost(string.Empty, IPAddress.None);
|
||||
|
||||
/// <summary>
|
||||
/// Time when last resolved in ticks.
|
||||
/// </summary>
|
||||
private DateTime? _lastResolved = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IP Addresses, attempting to resolve the name, if there are none.
|
||||
/// </summary>
|
||||
private IPAddress[] _addresses;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IPHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">Host name to assign.</param>
|
||||
public IPHost(string name)
|
||||
{
|
||||
HostName = name ?? throw new ArgumentNullException(nameof(name));
|
||||
_addresses = Array.Empty<IPAddress>();
|
||||
Resolved = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IPHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">Host name to assign.</param>
|
||||
/// <param name="address">Address to assign.</param>
|
||||
private IPHost(string name, IPAddress address)
|
||||
{
|
||||
HostName = name ?? throw new ArgumentNullException(nameof(name));
|
||||
_addresses = new IPAddress[] { address ?? throw new ArgumentNullException(nameof(address)) };
|
||||
Resolved = !address.Equals(IPAddress.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object's first IP address.
|
||||
/// </summary>
|
||||
public override IPAddress Address
|
||||
{
|
||||
get
|
||||
{
|
||||
return ResolveHost() ? this[0] : IPAddress.None;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
// Not implemented, as a host's address is determined by DNS.
|
||||
throw new NotImplementedException("The address of a host is determined by DNS.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object's first IP's subnet prefix.
|
||||
/// The setter does nothing, but shouldn't raise an exception.
|
||||
/// </summary>
|
||||
public override byte PrefixLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return (byte)(ResolveHost() ? 128 : 32);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
// Not implemented, as a host object can only have a prefix length of 128 (IPv6) or 32 (IPv4) prefix length,
|
||||
// which is automatically determined by it's IP type. Anything else is meaningless.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the address has a value.
|
||||
/// </summary>
|
||||
public bool HasAddress => _addresses.Length != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the host name of this object.
|
||||
/// </summary>
|
||||
public string HostName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this host has attempted to be resolved.
|
||||
/// </summary>
|
||||
public bool Resolved { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IP Addresses associated with this object.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of address.</param>
|
||||
public IPAddress this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ResolveHost();
|
||||
return index >= 0 && index < _addresses.Length ? _addresses[index] : IPAddress.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the host string.
|
||||
/// </summary>
|
||||
/// <param name="host">Host name to parse.</param>
|
||||
/// <param name="hostObj">Object representing the string, if it has successfully been parsed.</param>
|
||||
/// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
|
||||
public static bool TryParse(string host, out IPHost hostObj)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
{
|
||||
// See if it's an IPv6 with port address e.g. [::1]:120.
|
||||
int i = host.IndexOf("]:", StringComparison.OrdinalIgnoreCase);
|
||||
if (i != -1)
|
||||
{
|
||||
return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
// See if it's an IPv6 in [] with no port.
|
||||
i = host.IndexOf(']', StringComparison.OrdinalIgnoreCase);
|
||||
if (i != -1)
|
||||
{
|
||||
return TryParse(host.Remove(i - 1).TrimStart(' ', '['), out hostObj);
|
||||
}
|
||||
|
||||
// Is it a host or IPv4 with port?
|
||||
string[] hosts = host.Split(':');
|
||||
|
||||
if (hosts.Length > 2)
|
||||
{
|
||||
hostObj = new IPHost(string.Empty, IPAddress.None);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove port from IPv4 if it exists.
|
||||
host = hosts[0];
|
||||
|
||||
if (string.Equals("localhost", host, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
hostObj = new IPHost(host, new IPAddress(Ipv4Loopback));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IPNetAddress.TryParse(host, out IPNetAddress netIP))
|
||||
{
|
||||
// Host name is an ip address, so fake resolve.
|
||||
hostObj = new IPHost(host, netIP.Address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only thing left is to see if it's a host string.
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
{
|
||||
// Use regular expression as CheckHostName isn't RFC5892 compliant.
|
||||
// Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
|
||||
Regex re = new Regex(@"^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)$", RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
||||
if (re.Match(host).Success)
|
||||
{
|
||||
hostObj = new IPHost(host);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hostObj = IPHost.None;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the host string.
|
||||
/// </summary>
|
||||
/// <param name="host">Host name to parse.</param>
|
||||
/// <returns>Object representing the string, if it has successfully been parsed.</returns>
|
||||
public static IPHost Parse(string host)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
|
||||
{
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new InvalidCastException("Host does not contain a valid value. {host}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to parse the host string, ensuring that it resolves only to a specific IP type.
|
||||
/// </summary>
|
||||
/// <param name="host">Host name to parse.</param>
|
||||
/// <param name="family">Addressfamily filter.</param>
|
||||
/// <returns>Object representing the string, if it has successfully been parsed.</returns>
|
||||
public static IPHost Parse(string host, AddressFamily family)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(host) && IPHost.TryParse(host, out IPHost res))
|
||||
{
|
||||
if (family == AddressFamily.InterNetwork)
|
||||
{
|
||||
res.Remove(AddressFamily.InterNetworkV6);
|
||||
}
|
||||
else
|
||||
{
|
||||
res.Remove(AddressFamily.InterNetwork);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
throw new InvalidCastException("Host does not contain a valid value. {host}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Addresses that this item resolved to.
|
||||
/// </summary>
|
||||
/// <returns>IPAddress Array.</returns>
|
||||
public IPAddress[] GetAddresses()
|
||||
{
|
||||
ResolveHost();
|
||||
return _addresses;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Contains(IPAddress address)
|
||||
{
|
||||
if (address != null && !Address.Equals(IPAddress.None))
|
||||
{
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
foreach (var addr in GetAddresses())
|
||||
{
|
||||
if (address.Equals(addr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(IPObject? other)
|
||||
{
|
||||
if (other is IPHost otherObj)
|
||||
{
|
||||
// Do we have the name Hostname?
|
||||
if (string.Equals(otherObj.HostName, HostName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ResolveHost() || !otherObj.ResolveHost())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do any of our IP addresses match?
|
||||
foreach (IPAddress addr in _addresses)
|
||||
{
|
||||
foreach (IPAddress otherAddress in otherObj._addresses)
|
||||
{
|
||||
if (addr.Equals(otherAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsIP6()
|
||||
{
|
||||
// Returns true if interfaces are only IP6.
|
||||
if (ResolveHost())
|
||||
{
|
||||
foreach (IPAddress i in _addresses)
|
||||
{
|
||||
if (i.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
// StringBuilder not optimum here.
|
||||
string output = string.Empty;
|
||||
if (_addresses.Length > 0)
|
||||
{
|
||||
bool moreThanOne = _addresses.Length > 1;
|
||||
if (moreThanOne)
|
||||
{
|
||||
output = "[";
|
||||
}
|
||||
|
||||
foreach (var i in _addresses)
|
||||
{
|
||||
if (Address.Equals(IPAddress.None) && Address.AddressFamily == AddressFamily.Unspecified)
|
||||
{
|
||||
output += HostName + ",";
|
||||
}
|
||||
else if (i.Equals(IPAddress.Any))
|
||||
{
|
||||
output += "Any IP4 Address,";
|
||||
}
|
||||
else if (Address.Equals(IPAddress.IPv6Any))
|
||||
{
|
||||
output += "Any IP6 Address,";
|
||||
}
|
||||
else if (i.Equals(IPAddress.Broadcast))
|
||||
{
|
||||
output += "Any Address,";
|
||||
}
|
||||
else
|
||||
{
|
||||
output += $"{i}/32,";
|
||||
}
|
||||
}
|
||||
|
||||
output = output[0..^1];
|
||||
|
||||
if (moreThanOne)
|
||||
{
|
||||
output += "]";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
output = HostName;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Remove(AddressFamily family)
|
||||
{
|
||||
if (ResolveHost())
|
||||
{
|
||||
_addresses = _addresses.Where(p => p.AddressFamily != family).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Contains(IPObject address)
|
||||
{
|
||||
// An IPHost cannot contain another IPObject, it can only be equal.
|
||||
return Equals(address);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IPObject CalculateNetworkAddress()
|
||||
{
|
||||
var netAddr = NetworkAddressOf(this[0], PrefixLength);
|
||||
return new IPNetAddress(netAddr.Address, netAddr.PrefixLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve the ip address of a host.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if any addresses have been resolved, otherwise <c>false</c>.</returns>
|
||||
private bool ResolveHost()
|
||||
{
|
||||
// When was the last time we resolved?
|
||||
if (_lastResolved == null)
|
||||
{
|
||||
_lastResolved = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// If we haven't resolved before, or our timer has run out...
|
||||
if ((_addresses.Length == 0 && !Resolved) || (DateTime.UtcNow > _lastResolved?.AddMinutes(Timeout)))
|
||||
{
|
||||
_lastResolved = DateTime.UtcNow;
|
||||
ResolveHostInternal().GetAwaiter().GetResult();
|
||||
Resolved = true;
|
||||
}
|
||||
|
||||
return _addresses.Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task that looks up a Host name and returns its IP addresses.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
private async Task ResolveHostInternal()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(HostName))
|
||||
{
|
||||
// Resolves the host name - so save a DNS lookup.
|
||||
if (string.Equals(HostName, "localhost", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_addresses = new IPAddress[] { new IPAddress(Ipv4Loopback), new IPAddress(Ipv6Loopback) };
|
||||
return;
|
||||
}
|
||||
|
||||
if (Uri.CheckHostName(HostName).Equals(UriHostNameType.Dns))
|
||||
{
|
||||
try
|
||||
{
|
||||
IPHostEntry ip = await Dns.GetHostEntryAsync(HostName).ConfigureAwait(false);
|
||||
_addresses = ip.AddressList;
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
// Log and then ignore socket errors, as the result value will just be an empty array.
|
||||
Debug.WriteLine("GetHostEntryAsync failed with {Message}.", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
277
MediaBrowser.Common/Net/IPNetAddress.cs
Normal file
277
MediaBrowser.Common/Net/IPNetAddress.cs
Normal file
@ -0,0 +1,277 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// An object that holds and IP address and subnet mask.
|
||||
/// </summary>
|
||||
public class IPNetAddress : IPObject
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an IPNetAddress that has no value.
|
||||
/// </summary>
|
||||
public static readonly IPNetAddress None = new IPNetAddress(IPAddress.None);
|
||||
|
||||
/// <summary>
|
||||
/// IPv4 multicast address.
|
||||
/// </summary>
|
||||
public static readonly IPAddress SSDPMulticastIPv4 = IPAddress.Parse("239.255.255.250");
|
||||
|
||||
/// <summary>
|
||||
/// IPv6 local link multicast address.
|
||||
/// </summary>
|
||||
public static readonly IPAddress SSDPMulticastIPv6LinkLocal = IPAddress.Parse("ff02::C");
|
||||
|
||||
/// <summary>
|
||||
/// IPv6 site local multicast address.
|
||||
/// </summary>
|
||||
public static readonly IPAddress SSDPMulticastIPv6SiteLocal = IPAddress.Parse("ff05::C");
|
||||
|
||||
/// <summary>
|
||||
/// IP4Loopback address host.
|
||||
/// </summary>
|
||||
public static readonly IPNetAddress IP4Loopback = IPNetAddress.Parse("127.0.0.1/32");
|
||||
|
||||
/// <summary>
|
||||
/// IP6Loopback address host.
|
||||
/// </summary>
|
||||
public static readonly IPNetAddress IP6Loopback = IPNetAddress.Parse("::1");
|
||||
|
||||
/// <summary>
|
||||
/// Object's IP address.
|
||||
/// </summary>
|
||||
private IPAddress _address;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IPNetAddress"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">Address to assign.</param>
|
||||
public IPNetAddress(IPAddress address)
|
||||
{
|
||||
_address = address ?? throw new ArgumentNullException(nameof(address));
|
||||
PrefixLength = (byte)(address.AddressFamily == AddressFamily.InterNetwork ? 32 : 128);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IPNetAddress"/> class.
|
||||
/// </summary>
|
||||
/// <param name="address">IP Address.</param>
|
||||
/// <param name="prefixLength">Mask as a CIDR.</param>
|
||||
public IPNetAddress(IPAddress address, byte prefixLength)
|
||||
{
|
||||
if (address?.IsIPv4MappedToIPv6 ?? throw new ArgumentNullException(nameof(address)))
|
||||
{
|
||||
_address = address.MapToIPv4();
|
||||
}
|
||||
else
|
||||
{
|
||||
_address = address;
|
||||
}
|
||||
|
||||
PrefixLength = prefixLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object's IP address.
|
||||
/// </summary>
|
||||
public override IPAddress Address
|
||||
{
|
||||
get
|
||||
{
|
||||
return _address;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_address = value ?? IPAddress.None;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override byte PrefixLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Try to parse the address and subnet strings into an IPNetAddress object.
|
||||
/// </summary>
|
||||
/// <param name="addr">IP address to parse. Can be CIDR or X.X.X.X notation.</param>
|
||||
/// <param name="ip">Resultant object.</param>
|
||||
/// <returns>True if the values parsed successfully. False if not, resulting in the IP being null.</returns>
|
||||
public static bool TryParse(string addr, out IPNetAddress ip)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(addr))
|
||||
{
|
||||
addr = addr.Trim();
|
||||
|
||||
// Try to parse it as is.
|
||||
if (IPAddress.TryParse(addr, out IPAddress? res))
|
||||
{
|
||||
ip = new IPNetAddress(res);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is it a network?
|
||||
string[] tokens = addr.Split("/");
|
||||
|
||||
if (tokens.Length == 2)
|
||||
{
|
||||
tokens[0] = tokens[0].TrimEnd();
|
||||
tokens[1] = tokens[1].TrimStart();
|
||||
|
||||
if (IPAddress.TryParse(tokens[0], out res))
|
||||
{
|
||||
// Is the subnet part a cidr?
|
||||
if (byte.TryParse(tokens[1], out byte cidr))
|
||||
{
|
||||
ip = new IPNetAddress(res, cidr);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Is the subnet in x.y.a.b form?
|
||||
if (IPAddress.TryParse(tokens[1], out IPAddress? mask))
|
||||
{
|
||||
ip = new IPNetAddress(res, MaskToCidr(mask));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ip = None;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the string provided, throwing an exception if it is badly formed.
|
||||
/// </summary>
|
||||
/// <param name="addr">String to parse.</param>
|
||||
/// <returns>IPNetAddress object.</returns>
|
||||
public static IPNetAddress Parse(string addr)
|
||||
{
|
||||
if (TryParse(addr, out IPNetAddress o))
|
||||
{
|
||||
return o;
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unable to recognise object :" + addr);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Contains(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
var altAddress = NetworkAddressOf(address, PrefixLength);
|
||||
return NetworkAddress.Address.Equals(altAddress.Address) && NetworkAddress.PrefixLength >= altAddress.PrefixLength;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Contains(IPObject address)
|
||||
{
|
||||
if (address is IPHost addressObj && addressObj.HasAddress)
|
||||
{
|
||||
foreach (IPAddress addr in addressObj.GetAddresses())
|
||||
{
|
||||
if (Contains(addr))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (address is IPNetAddress netaddrObj)
|
||||
{
|
||||
// Have the same network address, but different subnets?
|
||||
if (NetworkAddress.Address.Equals(netaddrObj.NetworkAddress.Address))
|
||||
{
|
||||
return NetworkAddress.PrefixLength <= netaddrObj.PrefixLength;
|
||||
}
|
||||
|
||||
var altAddress = NetworkAddressOf(netaddrObj.Address, PrefixLength);
|
||||
return NetworkAddress.Address.Equals(altAddress.Address);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(IPObject? other)
|
||||
{
|
||||
if (other is IPNetAddress otherObj && !Address.Equals(IPAddress.None) && !otherObj.Address.Equals(IPAddress.None))
|
||||
{
|
||||
return Address.Equals(otherObj.Address) &&
|
||||
PrefixLength == otherObj.PrefixLength;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(IPAddress address)
|
||||
{
|
||||
if (address != null && !address.Equals(IPAddress.None) && !Address.Equals(IPAddress.None))
|
||||
{
|
||||
return address.Equals(Address);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a textual representation of this object.
|
||||
/// </summary>
|
||||
/// <param name="shortVersion">Set to true, if the subnet is to be excluded as part of the address.</param>
|
||||
/// <returns>String representation of this object.</returns>
|
||||
public string ToString(bool shortVersion)
|
||||
{
|
||||
if (!Address.Equals(IPAddress.None))
|
||||
{
|
||||
if (Address.Equals(IPAddress.Any))
|
||||
{
|
||||
return "Any IP4 Address";
|
||||
}
|
||||
|
||||
if (Address.Equals(IPAddress.IPv6Any))
|
||||
{
|
||||
return "Any IP6 Address";
|
||||
}
|
||||
|
||||
if (Address.Equals(IPAddress.Broadcast))
|
||||
{
|
||||
return "Any Address";
|
||||
}
|
||||
|
||||
if (shortVersion)
|
||||
{
|
||||
return Address.ToString();
|
||||
}
|
||||
|
||||
return $"{Address}/{PrefixLength}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IPObject CalculateNetworkAddress()
|
||||
{
|
||||
var value = NetworkAddressOf(_address, PrefixLength);
|
||||
return new IPNetAddress(value.Address, value.PrefixLength);
|
||||
}
|
||||
}
|
||||
}
|
406
MediaBrowser.Common/Net/IPObject.cs
Normal file
406
MediaBrowser.Common/Net/IPObject.cs
Normal file
@ -0,0 +1,406 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Base network object class.
|
||||
/// </summary>
|
||||
public abstract class IPObject : IEquatable<IPObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// IPv6 Loopback address.
|
||||
/// </summary>
|
||||
protected static readonly byte[] Ipv6Loopback = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
|
||||
/// <summary>
|
||||
/// IPv4 Loopback address.
|
||||
/// </summary>
|
||||
protected static readonly byte[] Ipv4Loopback = { 127, 0, 0, 1 };
|
||||
|
||||
/// <summary>
|
||||
/// The network address of this object.
|
||||
/// </summary>
|
||||
private IPObject? _networkAddress;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a user defined value that is associated with this object.
|
||||
/// </summary>
|
||||
public int Tag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object's IP address.
|
||||
/// </summary>
|
||||
public abstract IPAddress Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the object's network address.
|
||||
/// </summary>
|
||||
public IPObject NetworkAddress => _networkAddress ??= CalculateNetworkAddress();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the object's IP address.
|
||||
/// </summary>
|
||||
public abstract byte PrefixLength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the AddressFamily of this object.
|
||||
/// </summary>
|
||||
public AddressFamily AddressFamily
|
||||
{
|
||||
get
|
||||
{
|
||||
// Keep terms separate as Address performs other functions in inherited objects.
|
||||
IPAddress address = Address;
|
||||
return address.Equals(IPAddress.None) ? AddressFamily.Unspecified : address.AddressFamily;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the network address of an object.
|
||||
/// </summary>
|
||||
/// <param name="address">IP Address to convert.</param>
|
||||
/// <param name="prefixLength">Subnet prefix.</param>
|
||||
/// <returns>IPAddress.</returns>
|
||||
public static (IPAddress Address, byte PrefixLength) NetworkAddressOf(IPAddress address, byte prefixLength)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (IsLoopback(address))
|
||||
{
|
||||
return (Address: address, PrefixLength: prefixLength);
|
||||
}
|
||||
|
||||
// An ip address is just a list of bytes, each one representing a segment on the network.
|
||||
// This separates the IP address into octets and calculates how many octets will need to be altered or set to zero dependant upon the
|
||||
// prefix length value. eg. /16 on a 4 octet ip4 address (192.168.2.240) will result in the 2 and the 240 being zeroed out.
|
||||
// Where there is not an exact boundary (eg /23), mod is used to calculate how many bits of this value are to be kept.
|
||||
|
||||
// GetAddressBytes
|
||||
Span<byte> addressBytes = stackalloc byte[address.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
|
||||
address.TryWriteBytes(addressBytes, out _);
|
||||
|
||||
int div = prefixLength / 8;
|
||||
int mod = prefixLength % 8;
|
||||
if (mod != 0)
|
||||
{
|
||||
// Prefix length is counted right to left, so subtract 8 so we know how many bits to clear.
|
||||
mod = 8 - mod;
|
||||
|
||||
// Shift out the bits from the octet that we don't want, by moving right then back left.
|
||||
addressBytes[div] = (byte)((int)addressBytes[div] >> mod << mod);
|
||||
// Move on the next byte.
|
||||
div++;
|
||||
}
|
||||
|
||||
// Blank out the remaining octets from mod + 1 to the end of the byte array. (192.168.2.240/16 becomes 192.168.0.0)
|
||||
for (int octet = div; octet < addressBytes.Length; octet++)
|
||||
{
|
||||
addressBytes[octet] = 0;
|
||||
}
|
||||
|
||||
// Return the network address for the prefix.
|
||||
return (Address: new IPAddress(addressBytes), PrefixLength: prefixLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is a Loopback address.
|
||||
/// </summary>
|
||||
/// <param name="address">Value to test.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
public static bool IsLoopback(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (!address.Equals(IPAddress.None))
|
||||
{
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
return address.Equals(IPAddress.Loopback) || address.Equals(IPAddress.IPv6Loopback);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the ip address is an IP6 address.
|
||||
/// </summary>
|
||||
/// <param name="address">Value to test.</param>
|
||||
/// <returns>True if it is.</returns>
|
||||
public static bool IsIP6(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
return !address.Equals(IPAddress.None) && (address.AddressFamily == AddressFamily.InterNetworkV6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if the address in the private address range.
|
||||
/// </summary>
|
||||
/// <param name="address">Object to test.</param>
|
||||
/// <returns>True if it contains a private address.</returns>
|
||||
public static bool IsPrivateAddressRange(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (!address.Equals(IPAddress.None))
|
||||
{
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
// GetAddressBytes
|
||||
Span<byte> octet = stackalloc byte[4];
|
||||
address.TryWriteBytes(octet, out _);
|
||||
|
||||
return (octet[0] == 10)
|
||||
|| (octet[0] == 172 && octet[1] >= 16 && octet[1] <= 31) // RFC1918
|
||||
|| (octet[0] == 192 && octet[1] == 168) // RFC1918
|
||||
|| (octet[0] == 127); // RFC1122
|
||||
}
|
||||
else
|
||||
{
|
||||
// GetAddressBytes
|
||||
Span<byte> octet = stackalloc byte[16];
|
||||
address.TryWriteBytes(octet, out _);
|
||||
|
||||
uint word = (uint)(octet[0] << 8) + octet[1];
|
||||
|
||||
return (word >= 0xfe80 && word <= 0xfebf) // fe80::/10 :Local link.
|
||||
|| (word >= 0xfc00 && word <= 0xfdff); // fc00::/7 :Unique local address.
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the IPAddress contains an IP6 Local link address.
|
||||
/// </summary>
|
||||
/// <param name="address">IPAddress object to check.</param>
|
||||
/// <returns>True if it is a local link address.</returns>
|
||||
/// <remarks>
|
||||
/// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress
|
||||
/// it appears that the IPAddress.IsIPv6LinkLocal is out of date.
|
||||
/// </remarks>
|
||||
public static bool IsIPv6LinkLocal(IPAddress address)
|
||||
{
|
||||
if (address == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(address));
|
||||
}
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
{
|
||||
address = address.MapToIPv4();
|
||||
}
|
||||
|
||||
if (address.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// GetAddressBytes
|
||||
Span<byte> octet = stackalloc byte[16];
|
||||
address.TryWriteBytes(octet, out _);
|
||||
uint word = (uint)(octet[0] << 8) + octet[1];
|
||||
|
||||
return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
|
||||
/// </summary>
|
||||
/// <param name="cidr">Subnet mask in CIDR notation.</param>
|
||||
/// <param name="family">IPv4 or IPv6 family.</param>
|
||||
/// <returns>String value of the subnet mask in dotted decimal notation.</returns>
|
||||
public static IPAddress CidrToMask(byte cidr, AddressFamily family)
|
||||
{
|
||||
uint addr = 0xFFFFFFFF << (family == AddressFamily.InterNetwork ? 32 : 128 - cidr);
|
||||
addr = ((addr & 0xff000000) >> 24)
|
||||
| ((addr & 0x00ff0000) >> 8)
|
||||
| ((addr & 0x0000ff00) << 8)
|
||||
| ((addr & 0x000000ff) << 24);
|
||||
return new IPAddress(addr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a mask to a CIDR. IPv4 only.
|
||||
/// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
|
||||
/// </summary>
|
||||
/// <param name="mask">Subnet mask.</param>
|
||||
/// <returns>Byte CIDR representing the mask.</returns>
|
||||
public static byte MaskToCidr(IPAddress mask)
|
||||
{
|
||||
if (mask == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(mask));
|
||||
}
|
||||
|
||||
byte cidrnet = 0;
|
||||
if (!mask.Equals(IPAddress.Any))
|
||||
{
|
||||
// GetAddressBytes
|
||||
Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? 4 : 16];
|
||||
mask.TryWriteBytes(bytes, out _);
|
||||
|
||||
var zeroed = false;
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
|
||||
{
|
||||
if (zeroed)
|
||||
{
|
||||
// Invalid netmask.
|
||||
return (byte)~cidrnet;
|
||||
}
|
||||
|
||||
if ((v & 0x80) == 0)
|
||||
{
|
||||
zeroed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
cidrnet++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cidrnet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if this object is a Loopback address.
|
||||
/// </summary>
|
||||
/// <returns>True if it is.</returns>
|
||||
public virtual bool IsLoopback()
|
||||
{
|
||||
return IsLoopback(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all addresses of a specific type from this object.
|
||||
/// </summary>
|
||||
/// <param name="family">Type of address to remove.</param>
|
||||
public virtual void Remove(AddressFamily family)
|
||||
{
|
||||
// This method only performs a function in the IPHost implementation of IPObject.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests to see if this object is an IPv6 address.
|
||||
/// </summary>
|
||||
/// <returns>True if it is.</returns>
|
||||
public virtual bool IsIP6()
|
||||
{
|
||||
return IsIP6(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if this IP address is in the RFC private address range.
|
||||
/// </summary>
|
||||
/// <returns>True this object has a private address.</returns>
|
||||
public virtual bool IsPrivateAddressRange()
|
||||
{
|
||||
return IsPrivateAddressRange(Address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares this to the object passed as a parameter.
|
||||
/// </summary>
|
||||
/// <param name="ip">Object to compare to.</param>
|
||||
/// <returns>Equality result.</returns>
|
||||
public virtual bool Equals(IPAddress ip)
|
||||
{
|
||||
if (ip != null)
|
||||
{
|
||||
if (ip.IsIPv4MappedToIPv6)
|
||||
{
|
||||
ip = ip.MapToIPv4();
|
||||
}
|
||||
|
||||
return !Address.Equals(IPAddress.None) && Address.Equals(ip);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares this to the object passed as a parameter.
|
||||
/// </summary>
|
||||
/// <param name="other">Object to compare to.</param>
|
||||
/// <returns>Equality result.</returns>
|
||||
public virtual bool Equals(IPObject? other)
|
||||
{
|
||||
if (other != null)
|
||||
{
|
||||
return !Address.Equals(IPAddress.None) && Address.Equals(other.Address);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares the address in this object and the address in the object passed as a parameter.
|
||||
/// </summary>
|
||||
/// <param name="address">Object's IP address to compare to.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public abstract bool Contains(IPObject address);
|
||||
|
||||
/// <summary>
|
||||
/// Compares the address in this object and the address in the object passed as a parameter.
|
||||
/// </summary>
|
||||
/// <param name="address">Object's IP address to compare to.</param>
|
||||
/// <returns>Comparison result.</returns>
|
||||
public abstract bool Contains(IPAddress address);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Address.GetHashCode();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return Equals(obj as IPObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the network address of this object.
|
||||
/// </summary>
|
||||
/// <returns>Returns the network address of this object.</returns>
|
||||
protected abstract IPObject CalculateNetworkAddress();
|
||||
}
|
||||
}
|
262
MediaBrowser.Common/Net/NetworkExtensions.cs
Normal file
262
MediaBrowser.Common/Net/NetworkExtensions.cs
Normal file
@ -0,0 +1,262 @@
|
||||
#pragma warning disable CA1062 // Validate arguments of public methods
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Common.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="NetworkExtensions" />.
|
||||
/// </summary>
|
||||
public static class NetworkExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add an address to the collection.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="ip">Item to add.</param>
|
||||
public static void AddItem(this Collection<IPObject> source, IPAddress ip)
|
||||
{
|
||||
if (!source.ContainsAddress(ip))
|
||||
{
|
||||
source.Add(new IPNetAddress(ip, 32));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a network to the collection.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="item">Item to add.</param>
|
||||
public static void AddItem(this Collection<IPObject> source, IPObject item)
|
||||
{
|
||||
if (!source.ContainsAddress(item))
|
||||
{
|
||||
source.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts this object to a string.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <returns>Returns a string representation of this object.</returns>
|
||||
public static string AsString(this Collection<IPObject> source)
|
||||
{
|
||||
return $"[{string.Join(',', source)}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the collection contains an item with the ip address,
|
||||
/// or the ip address falls within any of the collection's network ranges.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="item">The item to look for.</param>
|
||||
/// <returns>True if the collection contains the item.</returns>
|
||||
public static bool ContainsAddress(this Collection<IPObject> source, IPAddress item)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
if (item.IsIPv4MappedToIPv6)
|
||||
{
|
||||
item = item.MapToIPv4();
|
||||
}
|
||||
|
||||
foreach (var i in source)
|
||||
{
|
||||
if (i.Contains(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the collection contains an item with the ip address,
|
||||
/// or the ip address falls within any of the collection's network ranges.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="item">The item to look for.</param>
|
||||
/// <returns>True if the collection contains the item.</returns>
|
||||
public static bool ContainsAddress(this Collection<IPObject> source, IPObject item)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(item));
|
||||
}
|
||||
|
||||
foreach (var i in source)
|
||||
{
|
||||
if (i.Contains(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compares two Collection{IPObject} objects. The order is ignored.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="dest">Item to compare to.</param>
|
||||
/// <returns>True if both are equal.</returns>
|
||||
public static bool Compare(this Collection<IPObject> source, Collection<IPObject> dest)
|
||||
{
|
||||
if (dest == null || source.Count != dest.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var sourceItem in source)
|
||||
{
|
||||
bool found = false;
|
||||
foreach (var destItem in dest)
|
||||
{
|
||||
if (sourceItem.Equals(destItem))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a collection containing the subnets of this collection given.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <returns>Collection{IPObject} object containing the subnets.</returns>
|
||||
public static Collection<IPObject> AsNetworks(this Collection<IPObject> source)
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
Collection<IPObject> res = new Collection<IPObject>();
|
||||
|
||||
foreach (IPObject i in source)
|
||||
{
|
||||
if (i is IPNetAddress nw)
|
||||
{
|
||||
// Add the subnet calculated from the interface address/mask.
|
||||
var na = nw.NetworkAddress;
|
||||
na.Tag = i.Tag;
|
||||
res.AddItem(na);
|
||||
}
|
||||
else if (i is IPHost ipHost)
|
||||
{
|
||||
// Flatten out IPHost and add all its ip addresses.
|
||||
foreach (var addr in ipHost.GetAddresses())
|
||||
{
|
||||
IPNetAddress host = new IPNetAddress(addr)
|
||||
{
|
||||
Tag = i.Tag
|
||||
};
|
||||
|
||||
res.AddItem(host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Excludes all the items from this list that are found in excludeList.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="excludeList">Items to exclude.</param>
|
||||
/// <returns>A new collection, with the items excluded.</returns>
|
||||
public static Collection<IPObject> Exclude(this Collection<IPObject> source, Collection<IPObject> excludeList)
|
||||
{
|
||||
if (source.Count == 0 || excludeList == null)
|
||||
{
|
||||
return new Collection<IPObject>(source);
|
||||
}
|
||||
|
||||
Collection<IPObject> results = new Collection<IPObject>();
|
||||
|
||||
bool found;
|
||||
foreach (var outer in source)
|
||||
{
|
||||
found = false;
|
||||
|
||||
foreach (var inner in excludeList)
|
||||
{
|
||||
if (outer.Equals(inner))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
results.AddItem(outer);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all items that co-exist in this object and target.
|
||||
/// </summary>
|
||||
/// <param name="source">The <see cref="Collection{IPObject}"/>.</param>
|
||||
/// <param name="target">Collection to compare with.</param>
|
||||
/// <returns>A collection containing all the matches.</returns>
|
||||
public static Collection<IPObject> Union(this Collection<IPObject> source, Collection<IPObject> target)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new Collection<IPObject>();
|
||||
}
|
||||
|
||||
if (target == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
}
|
||||
|
||||
Collection<IPObject> nc = new Collection<IPObject>();
|
||||
|
||||
foreach (IPObject i in source)
|
||||
{
|
||||
if (target.ContainsAddress(i))
|
||||
{
|
||||
nc.AddItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
return nc;
|
||||
}
|
||||
}
|
||||
}
|
@ -247,7 +247,23 @@ namespace MediaBrowser.Common.Plugins
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
|
||||
var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
|
||||
SaveConfiguration(config);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the current configuration to the file system.
|
||||
/// </summary>
|
||||
/// <param name="config">Configuration to save.</param>
|
||||
public virtual void SaveConfiguration(TConfigurationType config)
|
||||
{
|
||||
lock (_configurationSaveLock)
|
||||
{
|
||||
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
|
||||
|
||||
XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,12 +272,7 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// </summary>
|
||||
public virtual void SaveConfiguration()
|
||||
{
|
||||
lock (_configurationSaveLock)
|
||||
{
|
||||
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
|
||||
|
||||
XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
|
||||
}
|
||||
SaveConfiguration(Configuration);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -274,7 +285,7 @@ namespace MediaBrowser.Common.Plugins
|
||||
|
||||
Configuration = (TConfigurationType)configuration;
|
||||
|
||||
SaveConfiguration();
|
||||
SaveConfiguration(Configuration);
|
||||
|
||||
ConfigurationChanged?.Invoke(this, configuration);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -1099,12 +1099,12 @@ namespace MediaBrowser.Controller.Entities
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.Genres.Length > 0)
|
||||
if (request.Genres.Count > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.GenreIds.Length > 0)
|
||||
if (request.GenreIds.Count > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -1209,7 +1209,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.GenreIds.Length > 0)
|
||||
if (request.GenreIds.Count > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public string[] ExcludeInheritedTags { get; set; }
|
||||
|
||||
public string[] Genres { get; set; }
|
||||
public IReadOnlyList<string> Genres { get; set; }
|
||||
|
||||
public bool? IsSpecialSeason { get; set; }
|
||||
|
||||
@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public Guid[] StudioIds { get; set; }
|
||||
|
||||
public Guid[] GenreIds { get; set; }
|
||||
public IReadOnlyList<Guid> GenreIds { get; set; }
|
||||
|
||||
public ImageType[] ImageTypes { get; set; }
|
||||
|
||||
@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
|
||||
public double? MinCommunityRating { get; set; }
|
||||
|
||||
public Guid[] ChannelIds { get; set; }
|
||||
public IReadOnlyList<Guid> ChannelIds { get; set; }
|
||||
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
|
||||
|
@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
// Apply genre filter
|
||||
if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
|
||||
if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
// Apply genre filter
|
||||
if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id =>
|
||||
if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
|
||||
{
|
||||
var genreItem = libraryManager.GetItemById(id);
|
||||
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class EncodingHelper
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return "libopus";
|
||||
}
|
||||
|
||||
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// flac is experimental in mp4 muxer
|
||||
return "flac -strict -2";
|
||||
}
|
||||
|
||||
return codec.ToLowerInvariant();
|
||||
}
|
||||
|
||||
@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream.</param>
|
||||
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
|
||||
public bool IsH264(MediaStream stream)
|
||||
public static bool IsH264(MediaStream stream)
|
||||
{
|
||||
var codec = stream.Codec ?? string.Empty;
|
||||
|
||||
@ -581,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
public bool IsH265(MediaStream stream)
|
||||
public static bool IsH265(MediaStream stream)
|
||||
{
|
||||
var codec = stream.Codec ?? string.Empty;
|
||||
|
||||
@ -589,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
// TODO This is auto inserted into the mpegts mux so it might not be needed
|
||||
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
|
||||
public string GetBitStreamArgs(MediaStream stream)
|
||||
public static bool IsAAC(MediaStream stream)
|
||||
{
|
||||
var codec = stream.Codec ?? string.Empty;
|
||||
|
||||
return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
}
|
||||
|
||||
public static string GetBitStreamArgs(MediaStream stream)
|
||||
{
|
||||
// TODO This is auto inserted into the mpegts mux so it might not be needed.
|
||||
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
|
||||
if (IsH264(stream))
|
||||
{
|
||||
return "-bsf:v h264_mp4toannexb";
|
||||
@ -601,12 +615,44 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
return "-bsf:v hevc_mp4toannexb";
|
||||
}
|
||||
else if (IsAAC(stream))
|
||||
{
|
||||
// Convert adts header(mpegts) to asc header(mp4).
|
||||
return "-bsf:a aac_adtstoasc";
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
|
||||
{
|
||||
var bitStreamArgs = string.Empty;
|
||||
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
|
||||
|
||||
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
|
||||
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
|
||||
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
bitStreamArgs = GetBitStreamArgs(state.AudioStream);
|
||||
bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
|
||||
}
|
||||
|
||||
return bitStreamArgs;
|
||||
}
|
||||
|
||||
public static string GetSegmentFileExtension(string segmentContainer)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(segmentContainer))
|
||||
{
|
||||
return "." + segmentContainer;
|
||||
}
|
||||
|
||||
return ".ts";
|
||||
}
|
||||
|
||||
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
|
||||
{
|
||||
var bitrate = state.OutputVideoBitrate;
|
||||
@ -654,16 +700,30 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string NormalizeTranscodingLevel(string videoCodec, string level)
|
||||
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
|
||||
{
|
||||
// Clients may direct play higher than level 41, but there's no reason to transcode higher
|
||||
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
|
||||
&& requestLevel > 41
|
||||
&& (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
|
||||
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
|
||||
{
|
||||
return "41";
|
||||
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Transcode to level 5.0 and lower for maximum compatibility.
|
||||
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
|
||||
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
|
||||
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
|
||||
if (requestLevel >= 150)
|
||||
{
|
||||
return "150";
|
||||
}
|
||||
}
|
||||
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Clients may direct play higher than level 41, but there's no reason to transcode higher.
|
||||
if (requestLevel >= 41)
|
||||
{
|
||||
return "41";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return level;
|
||||
@ -766,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHlsVideoKeyFrameArguments(
|
||||
EncodingJobInfo state,
|
||||
string codec,
|
||||
int segmentLength,
|
||||
bool isEventPlaylist,
|
||||
int? startNumber)
|
||||
{
|
||||
var args = string.Empty;
|
||||
var gopArg = string.Empty;
|
||||
var keyFrameArg = string.Empty;
|
||||
if (isEventPlaylist)
|
||||
{
|
||||
keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
|
||||
segmentLength);
|
||||
}
|
||||
else if (startNumber.HasValue)
|
||||
{
|
||||
keyFrameArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
|
||||
startNumber.Value * segmentLength,
|
||||
segmentLength);
|
||||
}
|
||||
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
if (framerate.HasValue)
|
||||
{
|
||||
// This is to make sure keyframe interval is limited to our segment,
|
||||
// as forcing keyframes is not enough.
|
||||
// Example: we encoded half of desired length, then codec detected
|
||||
// scene cut and inserted a keyframe; next forced keyframe would
|
||||
// be created outside of segment, which breaks seeking.
|
||||
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
|
||||
gopArg = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0",
|
||||
Math.Ceiling(segmentLength * framerate.Value));
|
||||
}
|
||||
|
||||
// Unable to force key frames using these encoders, set key frames by GOP.
|
||||
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += gopArg;
|
||||
}
|
||||
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args += " " + keyFrameArg;
|
||||
}
|
||||
else
|
||||
{
|
||||
args += " " + keyFrameArg + gopArg;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the video bitrate to specify on the command line.
|
||||
/// </summary>
|
||||
@ -773,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
var param = string.Empty;
|
||||
|
||||
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -pix_fmt yuv420p";
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var videoStream = state.VideoStream;
|
||||
var isColorDepth10 = IsColorDepth10(state);
|
||||
|
||||
if (isColorDepth10
|
||||
&& _mediaEncoder.SupportsHwaccel("opencl")
|
||||
&& encodingOptions.EnableTonemapping
|
||||
&& !string.IsNullOrEmpty(videoStream.VideoRange)
|
||||
&& videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -pix_fmt nv12";
|
||||
}
|
||||
else
|
||||
{
|
||||
param += " -pix_fmt yuv420p";
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -pix_fmt nv21";
|
||||
}
|
||||
|
||||
var isVc1 = state.VideoStream != null &&
|
||||
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
|
||||
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
|
||||
@ -781,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
|
||||
{
|
||||
param += "-preset " + encodingOptions.EncoderPreset;
|
||||
param += " -preset " + encodingOptions.EncoderPreset;
|
||||
}
|
||||
else
|
||||
{
|
||||
param += "-preset " + defaultPreset;
|
||||
param += " -preset " + defaultPreset;
|
||||
}
|
||||
|
||||
int encodeCrf = encodingOptions.H264Crf;
|
||||
@ -809,38 +976,40 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
param += " -crf " + defaultCrf;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv)
|
||||
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|
||||
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
|
||||
{
|
||||
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
|
||||
|
||||
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
param += "-preset " + encodingOptions.EncoderPreset;
|
||||
param += " -preset " + encodingOptions.EncoderPreset;
|
||||
}
|
||||
else
|
||||
{
|
||||
param += "-preset 7";
|
||||
param += " -preset 7";
|
||||
}
|
||||
|
||||
param += " -look_ahead 0";
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
|
||||
{
|
||||
// following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead.
|
||||
switch (encodingOptions.EncoderPreset)
|
||||
{
|
||||
case "veryslow":
|
||||
|
||||
param += "-preset slow"; // lossless is only supported on maxwell and newer(2014+)
|
||||
param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+)
|
||||
break;
|
||||
|
||||
case "slow":
|
||||
case "slower":
|
||||
param += "-preset slow";
|
||||
param += " -preset slow";
|
||||
break;
|
||||
|
||||
case "medium":
|
||||
param += "-preset medium";
|
||||
param += " -preset medium";
|
||||
break;
|
||||
|
||||
case "fast":
|
||||
@ -848,27 +1017,27 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
case "veryfast":
|
||||
case "superfast":
|
||||
case "ultrafast":
|
||||
param += "-preset fast";
|
||||
param += " -preset fast";
|
||||
break;
|
||||
|
||||
default:
|
||||
param += "-preset default";
|
||||
param += " -preset default";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|
||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
|
||||
{
|
||||
switch (encodingOptions.EncoderPreset)
|
||||
{
|
||||
case "veryslow":
|
||||
case "slow":
|
||||
case "slower":
|
||||
param += "-quality quality";
|
||||
param += " -quality quality";
|
||||
break;
|
||||
|
||||
case "medium":
|
||||
param += "-quality balanced";
|
||||
param += " -quality balanced";
|
||||
break;
|
||||
|
||||
case "fast":
|
||||
@ -876,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
case "veryfast":
|
||||
case "superfast":
|
||||
case "ultrafast":
|
||||
param += "-quality speed";
|
||||
param += " -quality speed";
|
||||
break;
|
||||
|
||||
default:
|
||||
param += "-quality speed";
|
||||
param += " -quality speed";
|
||||
break;
|
||||
}
|
||||
|
||||
@ -896,6 +1065,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// Enhance workload when tone mapping with AMF on some APUs
|
||||
param += " -preanalysis true";
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -header_insertion_mode gop -gops_per_idr 1";
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
|
||||
{
|
||||
@ -917,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
profileScore = Math.Min(profileScore, 2);
|
||||
|
||||
// http://www.webmproject.org/docs/encoder-parameters/
|
||||
param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
|
||||
param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
|
||||
profileScore.ToString(_usCulture),
|
||||
crf,
|
||||
qmin,
|
||||
@ -925,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
|
||||
param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
|
||||
{
|
||||
param += "-qmin 2";
|
||||
param += " -qmin 2";
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += "-mbd 2";
|
||||
param += " -mbd 2";
|
||||
}
|
||||
|
||||
param += GetVideoBitrateParam(state, videoEncoder);
|
||||
@ -945,11 +1119,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
var targetVideoCodec = state.ActualOutputVideoCodec;
|
||||
if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
targetVideoCodec = "hevc";
|
||||
}
|
||||
|
||||
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
|
||||
profile = Regex.Replace(profile, @"\s+", String.Empty);
|
||||
|
||||
// vaapi does not support Baseline profile, force Constrained Baseline in this case,
|
||||
// which is compatible (and ugly)
|
||||
// Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
|
||||
if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
&& profile != null
|
||||
&& profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
profile = "high";
|
||||
}
|
||||
|
||||
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
|
||||
// which is compatible (and ugly).
|
||||
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& profile != null
|
||||
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
@ -957,13 +1145,31 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
profile = "constrained_baseline";
|
||||
}
|
||||
|
||||
// libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
|
||||
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||
&& profile != null
|
||||
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
profile = "baseline";
|
||||
}
|
||||
|
||||
// Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
|
||||
if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
|
||||
&& profile != null
|
||||
&& profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
profile = "main";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(profile))
|
||||
{
|
||||
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// not supported by h264_omx
|
||||
param += " -profile:v " + profile;
|
||||
param += " -profile:v:0 " + profile;
|
||||
}
|
||||
}
|
||||
|
||||
@ -971,55 +1177,35 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (!string.IsNullOrEmpty(level))
|
||||
{
|
||||
level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
|
||||
level = NormalizeTranscodingLevel(state, level);
|
||||
|
||||
// h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
|
||||
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
|
||||
// libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
|
||||
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
switch (level)
|
||||
param += " -level " + level;
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// hevc_qsv use -level 51 instead of -level 153.
|
||||
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
|
||||
{
|
||||
case "30":
|
||||
param += " -level 3.0";
|
||||
break;
|
||||
case "31":
|
||||
param += " -level 3.1";
|
||||
break;
|
||||
case "32":
|
||||
param += " -level 3.2";
|
||||
break;
|
||||
case "40":
|
||||
param += " -level 4.0";
|
||||
break;
|
||||
case "41":
|
||||
param += " -level 4.1";
|
||||
break;
|
||||
case "42":
|
||||
param += " -level 4.2";
|
||||
break;
|
||||
case "50":
|
||||
param += " -level 5.0";
|
||||
break;
|
||||
case "51":
|
||||
param += " -level 5.1";
|
||||
break;
|
||||
case "52":
|
||||
param += " -level 5.2";
|
||||
break;
|
||||
default:
|
||||
param += " -level " + level;
|
||||
break;
|
||||
param += " -level " + hevcLevel / 3;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// nvenc doesn't decode with param -level set ?!
|
||||
// TODO:
|
||||
param += " -level " + level;
|
||||
}
|
||||
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// level option may cause NVENC to fail.
|
||||
// NVENC cannot adjust the given level, just throw an error.
|
||||
}
|
||||
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
|
||||
|| !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -level " + level;
|
||||
}
|
||||
@ -1032,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// todo
|
||||
}
|
||||
|
||||
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param = "-pix_fmt yuv420p " + param;
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var videoStream = state.VideoStream;
|
||||
var isColorDepth10 = IsColorDepth10(state);
|
||||
|
||||
if (isColorDepth10
|
||||
&& _mediaEncoder.SupportsHwaccel("opencl")
|
||||
&& encodingOptions.EnableTonemapping
|
||||
&& !string.IsNullOrEmpty(videoStream.VideoRange)
|
||||
&& videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param = "-pix_fmt nv12 " + param;
|
||||
}
|
||||
else
|
||||
{
|
||||
param = "-pix_fmt yuv420p " + param;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param = "-pix_fmt nv21 " + param;
|
||||
// libx265 only accept level option in -x265-params.
|
||||
// level option may cause libx265 to fail.
|
||||
// libx265 cannot adjust the given level, just throw an error.
|
||||
// TODO: set fine tuned params.
|
||||
param += " -x265-params:0 no-info=1";
|
||||
}
|
||||
|
||||
return param;
|
||||
@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return .5;
|
||||
return .6;
|
||||
}
|
||||
|
||||
return 1;
|
||||
@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
|
||||
{
|
||||
if (audioStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (request.AudioBitRate.HasValue)
|
||||
{
|
||||
// Don't encode any higher than this
|
||||
return Math.Min(384000, request.AudioBitRate.Value);
|
||||
}
|
||||
|
||||
// Empty bitrate area is not allow on iOS
|
||||
// Default audio bitrate to 128K if it is not being requested
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
|
||||
return 128000;
|
||||
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
|
||||
}
|
||||
|
||||
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
|
||||
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
|
||||
{
|
||||
if (audioStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (audioBitRate.HasValue)
|
||||
if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
|
||||
{
|
||||
// Don't encode any higher than this
|
||||
return Math.Min(384000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
|
||||
{
|
||||
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if ((audioStream.Channels ?? 0) >= 6)
|
||||
{
|
||||
return Math.Min(640000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
return Math.Min(384000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if ((audioStream.Channels ?? 0) >= 6)
|
||||
{
|
||||
return Math.Min(3584000, audioBitRate.Value);
|
||||
}
|
||||
|
||||
return Math.Min(1536000, audioBitRate.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty bitrate area is not allow on iOS
|
||||
// Default audio bitrate to 128K if it is not being requested
|
||||
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
|
||||
@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
if (filters.Count > 0)
|
||||
{
|
||||
return "-af \"" + string.Join(",", filters) + "\"";
|
||||
return " -af \"" + string.Join(",", filters) + "\"";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>System.Nullable{System.Int32}.</returns>
|
||||
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
|
||||
{
|
||||
if (audioStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var request = state.BaseRequest;
|
||||
|
||||
var inputChannels = audioStream?.Channels;
|
||||
@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// libmp3lame currently only supports two channel output
|
||||
transcoderChannelLimit = 2;
|
||||
}
|
||||
else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
// aac is able to handle 8ch(7.1 layout)
|
||||
transcoderChannelLimit = 8;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
|
||||
@ -1708,7 +1885,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// For QSV, feed it into hardware encoder now
|
||||
if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
videoSizeParam += ",hwupload=extra_hw_frames=64";
|
||||
}
|
||||
@ -1729,7 +1907,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
|
||||
|
||||
// When the input may or may not be hardware VAAPI decodable
|
||||
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
/*
|
||||
[base]: HW scaling video to OutputSize
|
||||
@ -1741,7 +1920,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
|
||||
else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
|
||||
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
|
||||
&& (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
/*
|
||||
[base]: SW scaling video to OutputSize
|
||||
@ -1750,7 +1930,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
*/
|
||||
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
|
||||
}
|
||||
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
/*
|
||||
QSV in FFMpeg can now setup hardware overlay for transcodes.
|
||||
@ -1776,7 +1957,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
videoSizeParam);
|
||||
}
|
||||
|
||||
private (int? width, int? height) GetFixedOutputSize(
|
||||
public static (int? width, int? height) GetFixedOutputSize(
|
||||
int? videoWidth,
|
||||
int? videoHeight,
|
||||
int? requestedWidth,
|
||||
@ -1836,7 +2017,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
requestedMaxHeight);
|
||||
|
||||
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
|
||||
&& width.HasValue
|
||||
&& height.HasValue)
|
||||
{
|
||||
@ -1845,7 +2028,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// output dimensions. Output dimensions are guaranteed to be even.
|
||||
var outputWidth = width.Value;
|
||||
var outputHeight = height.Value;
|
||||
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
|
||||
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase);
|
||||
var isDeintEnabled = state.DeInterlace("h264", true)
|
||||
|| state.DeInterlace("avc", true)
|
||||
|| state.DeInterlace("h265", true)
|
||||
@ -2107,10 +2291,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
|
||||
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
|
||||
var isColorDepth10 = IsColorDepth10(state);
|
||||
|
||||
@ -2185,6 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
filters.Add("hwdownload");
|
||||
|
||||
if (isLibX264Encoder
|
||||
|| isLibX265Encoder
|
||||
|| hasGraphicalSubs
|
||||
|| (isNvdecHevcDecoder && isDeinterlaceHevc)
|
||||
|| (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
|
||||
@ -2195,20 +2383,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// When the input may or may not be hardware VAAPI decodable
|
||||
if (isVaapiH264Encoder)
|
||||
if (isVaapiH264Encoder || isVaapiHevcEncoder)
|
||||
{
|
||||
filters.Add("format=nv12|vaapi");
|
||||
filters.Add("hwupload");
|
||||
}
|
||||
|
||||
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
|
||||
else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
|
||||
else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder))
|
||||
{
|
||||
filters.Add("hwupload=extra_hw_frames=64");
|
||||
}
|
||||
|
||||
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
|
||||
else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
|
||||
else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder))
|
||||
{
|
||||
var codec = videoStream.Codec.ToLowerInvariant();
|
||||
|
||||
@ -2250,7 +2438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
// Add software deinterlace filter before scaling filter
|
||||
if ((isDeinterlaceH264 || isDeinterlaceHevc)
|
||||
&& !isVaapiH264Encoder
|
||||
&& !isVaapiHevcEncoder
|
||||
&& !isQsvH264Encoder
|
||||
&& !isQsvHevcEncoder
|
||||
&& !isNvdecH264Decoder)
|
||||
{
|
||||
if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
|
||||
@ -2289,7 +2479,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
|
||||
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
|
||||
if (isVaapiH264Encoder)
|
||||
if (isVaapiH264Encoder || isVaapiHevcEncoder)
|
||||
{
|
||||
if (hasTextSubs)
|
||||
{
|
||||
@ -2562,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
public void AttachMediaSourceInfo(
|
||||
EncodingJobInfo state,
|
||||
EncodingOptions encodingOptions,
|
||||
MediaSourceInfo mediaSource,
|
||||
string requestedUrl)
|
||||
{
|
||||
@ -2692,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
|
||||
?? state.SupportedAudioCodecs.FirstOrDefault();
|
||||
}
|
||||
|
||||
var supportedVideoCodecs = state.SupportedVideoCodecs;
|
||||
if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0)
|
||||
{
|
||||
var supportedVideoCodecsList = supportedVideoCodecs.ToList();
|
||||
|
||||
ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
|
||||
|
||||
state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
|
||||
|
||||
request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
|
||||
{
|
||||
// Nothing to do here
|
||||
// No need to shift if there is only one supported audio codec.
|
||||
if (audioCodecs.Count < 2)
|
||||
{
|
||||
return;
|
||||
@ -2724,6 +2927,34 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
}
|
||||
}
|
||||
|
||||
private void ShiftVideoCodecsIfNeeded(List<string> videoCodecs, EncodingOptions encodingOptions)
|
||||
{
|
||||
// Shift hevc/h265 to the end of list if hevc encoding is not allowed.
|
||||
if (encodingOptions.AllowHevcEncoding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to shift if there is only one supported video codec.
|
||||
if (videoCodecs.Count < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var shiftVideoCodecs = new[] { "hevc", "h265" };
|
||||
if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var removed = shiftVideoCodecs[0];
|
||||
videoCodecs.RemoveAt(0);
|
||||
videoCodecs.Add(removed);
|
||||
}
|
||||
}
|
||||
|
||||
private void NormalizeSubtitleEmbed(EncodingJobInfo state)
|
||||
{
|
||||
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
|
||||
@ -3357,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
|
||||
}
|
||||
|
||||
args += " " + GetAudioFilterParam(state, encodingOptions, false);
|
||||
args += GetAudioFilterParam(state, encodingOptions, false);
|
||||
|
||||
return args;
|
||||
}
|
||||
|
@ -593,6 +593,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
if (VideoStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
|
||||
{
|
||||
return VideoStream?.Codec;
|
||||
@ -606,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AudioStream == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
|
||||
{
|
||||
return AudioStream?.Codec;
|
||||
|
@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists
|
||||
/// <param name="itemIds">The item ids.</param>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId);
|
||||
Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes from playlist.
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user