Merge branch 'master' into NetworkPR2

This commit is contained in:
BaronGreenback 2020-11-21 00:35:53 +00:00 committed by GitHub
commit 7a6063ed41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 2378 additions and 2735 deletions

View File

@ -30,11 +30,11 @@ jobs:
# This is required for the SonarCloud analyzer # This is required for the SonarCloud analyzer
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Install .NET Core SDK 2.1" displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest') condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs: inputs:
packageType: sdk packageType: sdk
version: '2.1.805' version: '5.x'
- task: UseDotNet@2 - task: UseDotNet@2
displayName: "Update DotNet" displayName: "Update DotNet"

View File

@ -1,7 +1,7 @@
namespace Emby.Dlna.Common namespace Emby.Dlna.Common
{ {
/// <summary> /// <summary>
/// DLNA Query parameter type, used when quering DLNA devices via SOAP. /// DLNA Query parameter type, used when querying DLNA devices via SOAP.
/// </summary> /// </summary>
public class Argument public class Argument
{ {

View File

@ -2,8 +2,14 @@
namespace Emby.Dlna.Configuration namespace Emby.Dlna.Configuration
{ {
/// <summary>
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
/// </summary>
public class DlnaOptions public class DlnaOptions
{ {
/// <summary>
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
/// </summary>
public DlnaOptions() public DlnaOptions()
{ {
EnablePlayTo = true; EnablePlayTo = true;
@ -11,23 +17,76 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true; BlastAliveMessages = true;
SendOnlyMatchedHost = true; SendOnlyMatchedHost = true;
ClientDiscoveryIntervalSeconds = 60; ClientDiscoveryIntervalSeconds = 60;
BlastAliveMessageIntervalSeconds = 1800; AliveMessageIntervalSeconds = 1800;
} }
/// <summary>
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna playTo subsystem.
/// </summary>
public bool EnablePlayTo { get; set; } public bool EnablePlayTo { get; set; }
/// <summary>
/// Gets or sets a value indicating whether gets or sets a value to indicate the status of the dlna server subsystem.
/// </summary>
public bool EnableServer { get; set; } public bool EnableServer { get; set; }
/// <summary>
/// Gets or sets a value indicating whether detailed dlna server logs are 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 bool EnableDebugLog { get; set; } public bool EnableDebugLog { get; set; }
public bool BlastAliveMessages { get; set; } /// <summary>
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
public bool SendOnlyMatchedHost { get; set; } /// If the setting "Emby.Dlna.PlayTo": "Debug" msut be set in logging.default.json for this property to work.
/// </summary>
public bool EnablePlayToTracing { get; set; }
/// <summary>
/// Gets or sets the ssdp client discovery interval time (in seconds).
/// This is the time after which the server will send a ssdp search request.
/// </summary>
public int ClientDiscoveryIntervalSeconds { get; set; } public int ClientDiscoveryIntervalSeconds { get; set; }
public int BlastAliveMessageIntervalSeconds { get; set; } /// <summary>
/// Gets or sets the frequency at which ssdp alive notifications are transmitted.
/// </summary>
public int AliveMessageIntervalSeconds { get; set; }
/// <summary>
/// Gets or sets the frequency at which ssdp alive notifications are transmitted. MIGRATING - TO BE REMOVED ONCE WEB HAS BEEN ALTERED.
/// </summary>
public int BlastAliveMessageIntervalSeconds
{
get
{
return AliveMessageIntervalSeconds;
}
set
{
AliveMessageIntervalSeconds = value;
}
}
/// <summary>
/// Gets or sets the default user account that the dlna server uses.
/// </summary>
public string DefaultUserId { get; set; } public string DefaultUserId { get; set; }
/// <summary>
/// Gets or sets a value indicating whether playTo device profiles should be created.
/// </summary>
public bool AutoCreatePlayToProfiles { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to blast alive messages.
/// </summary>
public bool BlastAliveMessages { get; set; } = true;
/// <summary>
/// gets or sets a value indicating whether to send only matched host.
/// </summary>
public bool SendOnlyMatchedHost { get; set; } = true;
} }
} }

View File

@ -9,11 +9,21 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
/// <summary>
/// Defines the <see cref="ConnectionManagerService" />.
/// </summary>
public class ConnectionManagerService : BaseService, IConnectionManager public class ConnectionManagerService : BaseService, IConnectionManager
{ {
private readonly IDlnaManager _dlna; private readonly IDlnaManager _dlna;
private readonly IServerConfigurationManager _config; private readonly IServerConfigurationManager _config;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectionManagerService"/> class.
/// </summary>
/// <param name="dlna">The <see cref="IDlnaManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ConnectionManagerService"/> instance.</param>
/// <param name="logger">The <see cref="ILogger{ConnectionManagerService}"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
/// <param name="httpClientFactory">The <see cref="IHttpClientFactory"/> for use with the <see cref="ConnectionManagerService"/> instance..</param>
public ConnectionManagerService( public ConnectionManagerService(
IDlnaManager dlna, IDlnaManager dlna,
IServerConfigurationManager config, IServerConfigurationManager config,
@ -28,7 +38,7 @@ namespace Emby.Dlna.ConnectionManager
/// <inheritdoc /> /// <inheritdoc />
public string GetServiceXml() public string GetServiceXml()
{ {
return new ConnectionManagerXmlBuilder().GetXml(); return ConnectionManagerXmlBuilder.GetXml();
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -6,45 +6,57 @@ using Emby.Dlna.Service;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
public class ConnectionManagerXmlBuilder /// <summary>
/// Defines the <see cref="ConnectionManagerXmlBuilder" />.
/// </summary>
public static class ConnectionManagerXmlBuilder
{ {
public string GetXml() /// <summary>
/// Gets the ConnectionManager:1 service template.
/// See http://upnp.org/specs/av/UPnP-av-ConnectionManager-v1-Service.pdf.
/// </summary>
/// <returns>An XML description of this service.</returns>
public static string GetXml()
{ {
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(), GetStateVariables()); return new ServiceXmlBuilder().GetXml(ServiceActionListBuilder.GetActions(), GetStateVariables());
} }
/// <summary>
/// Get the list of state variables for this invocation.
/// </summary>
/// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
private static IEnumerable<StateVariable> GetStateVariables() private static IEnumerable<StateVariable> GetStateVariables()
{ {
var list = new List<StateVariable>(); var list = new List<StateVariable>
list.Add(new StateVariable
{ {
Name = "SourceProtocolInfo", new StateVariable
DataType = "string", {
SendsEvents = true Name = "SourceProtocolInfo",
}); DataType = "string",
SendsEvents = true
},
list.Add(new StateVariable new StateVariable
{ {
Name = "SinkProtocolInfo", Name = "SinkProtocolInfo",
DataType = "string", DataType = "string",
SendsEvents = true SendsEvents = true
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "CurrentConnectionIDs", Name = "CurrentConnectionIDs",
DataType = "string", DataType = "string",
SendsEvents = true SendsEvents = true
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionStatus", Name = "A_ARG_TYPE_ConnectionStatus",
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new[] AllowedValues = new[]
{ {
"OK", "OK",
"ContentFormatMismatch", "ContentFormatMismatch",
@ -52,55 +64,56 @@ namespace Emby.Dlna.ConnectionManager
"UnreliableChannel", "UnreliableChannel",
"Unknown" "Unknown"
} }
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionManager", Name = "A_ARG_TYPE_ConnectionManager",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_Direction", Name = "A_ARG_TYPE_Direction",
DataType = "string", DataType = "string",
SendsEvents = false, SendsEvents = false,
AllowedValues = new[] AllowedValues = new[]
{ {
"Output", "Output",
"Input" "Input"
} }
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_ProtocolInfo", Name = "A_ARG_TYPE_ProtocolInfo",
DataType = "string", DataType = "string",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_ConnectionID", Name = "A_ARG_TYPE_ConnectionID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_AVTransportID", Name = "A_ARG_TYPE_AVTransportID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}); },
list.Add(new StateVariable new StateVariable
{ {
Name = "A_ARG_TYPE_RcsID", Name = "A_ARG_TYPE_RcsID",
DataType = "ui4", DataType = "ui4",
SendsEvents = false SendsEvents = false
}); }
};
return list; return list;
} }

View File

@ -11,10 +11,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler public class ControlHandler : BaseControlHandler
{ {
private readonly DeviceProfile _profile; private readonly DeviceProfile _profile;
/// <summary>
/// Initializes a new instance of the <see cref="ControlHandler"/> class.
/// </summary>
/// <param name="config">The <see cref="IServerConfigurationManager"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="logger">The <see cref="ILogger"/> for use with the <see cref="ControlHandler"/> instance.</param>
/// <param name="profile">The <see cref="DeviceProfile"/> for use with the <see cref="ControlHandler"/> instance.</param>
public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile) public ControlHandler(IServerConfigurationManager config, ILogger logger, DeviceProfile profile)
: base(config, logger) : base(config, logger)
{ {
@ -33,6 +42,10 @@ namespace Emby.Dlna.ConnectionManager
throw new ResourceNotFoundException("Unexpected control request name: " + methodName); throw new ResourceNotFoundException("Unexpected control request name: " + methodName);
} }
/// <summary>
/// Builds the response to the GetProtocolInfo request.
/// </summary>
/// <param name="xmlWriter">The <see cref="XmlWriter"/>.</param>
private void HandleGetProtocolInfo(XmlWriter xmlWriter) private void HandleGetProtocolInfo(XmlWriter xmlWriter)
{ {
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo); xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);

View File

@ -5,9 +5,16 @@ using Emby.Dlna.Common;
namespace Emby.Dlna.ConnectionManager namespace Emby.Dlna.ConnectionManager
{ {
public class ServiceActionListBuilder /// <summary>
/// Defines the <see cref="ServiceActionListBuilder" />.
/// </summary>
public static class ServiceActionListBuilder
{ {
public IEnumerable<ServiceAction> GetActions() /// <summary>
/// Returns an enumerable of the ConnectionManagar:1 DLNA actions.
/// </summary>
/// <returns>An <see cref="IEnumerable{ServiceAction}"/>.</returns>
public static IEnumerable<ServiceAction> GetActions()
{ {
var list = new List<ServiceAction> var list = new List<ServiceAction>
{ {
@ -21,6 +28,10 @@ namespace Emby.Dlna.ConnectionManager
return list; return list;
} }
/// <summary>
/// Returns the action details for "PrepareForConnection".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction PrepareForConnection() private static ServiceAction PrepareForConnection()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -80,6 +91,10 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
/// <summary>
/// Returns the action details for "GetCurrentConnectionInfo".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionInfo() private static ServiceAction GetCurrentConnectionInfo()
{ {
var action = new ServiceAction var action = new ServiceAction
@ -146,7 +161,11 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
private ServiceAction GetProtocolInfo() /// <summary>
/// Returns the action details for "GetProtocolInfo".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetProtocolInfo()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@ -170,7 +189,11 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
private ServiceAction GetCurrentConnectionIDs() /// <summary>
/// Returns the action details for "GetCurrentConnectionIDs".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionIDs()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {
@ -187,7 +210,11 @@ namespace Emby.Dlna.ConnectionManager
return action; return action;
} }
private ServiceAction ConnectionComplete() /// <summary>
/// Returns the action details for "ConnectionComplete".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction ConnectionComplete()
{ {
var action = new ServiceAction var action = new ServiceAction
{ {

View File

@ -1674,7 +1674,7 @@ namespace Emby.Dlna.ContentDirectory
} }
/// <summary> /// <summary>
/// Retreives the ServerItem id. /// Retrieves the ServerItem id.
/// </summary> /// </summary>
/// <param name="id">The id<see cref="string"/>.</param> /// <param name="id">The id<see cref="string"/>.</param>
/// <returns>The <see cref="ServerItem"/>.</returns> /// <returns>The <see cref="ServerItem"/>.</returns>

View File

@ -484,10 +484,10 @@ namespace Emby.Dlna
/// <summary> /// <summary>
/// Recreates the object using serialization, to ensure it's not a subclass. /// Recreates the object using serialization, to ensure it's not a subclass.
/// If it's a subclass it may not serlialize properly to xml (different root element tag name). /// If it's a subclass it may not serialize properly to xml (different root element tag name).
/// </summary> /// </summary>
/// <param name="profile">The device profile.</param> /// <param name="profile">The device profile.</param>
/// <returns>The reserialized device profile.</returns> /// <returns>The re-serialized device profile.</returns>
private DeviceProfile ReserializeProfile(DeviceProfile profile) private DeviceProfile ReserializeProfile(DeviceProfile profile)
{ {
if (profile.GetType() == typeof(DeviceProfile)) if (profile.GetType() == typeof(DeviceProfile))

View File

@ -480,7 +480,7 @@ namespace Emby.Dlna.PlayTo
return; return;
} }
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive // If we're not playing anything make sure we don't get data more often than necessary to keep the Session alive
if (transportState.Value == TransportState.Stopped) if (transportState.Value == TransportState.Stopped)
{ {
RestartTimerInactive(); RestartTimerInactive();
@ -775,7 +775,7 @@ namespace Emby.Dlna.PlayTo
if (track == null) if (track == null)
{ {
// If track is null, some vendors do this, use GetMediaInfo instead // If track is null, some vendors do this, use GetMediaInfo instead.
return (true, null); return (true, null);
} }
@ -812,7 +812,7 @@ namespace Emby.Dlna.PlayTo
private XElement ParseResponse(string xml) private XElement ParseResponse(string xml)
{ {
// Handle different variations sent back by devices // Handle different variations sent back by devices.
try try
{ {
return XElement.Parse(xml); return XElement.Parse(xml);
@ -821,7 +821,7 @@ namespace Emby.Dlna.PlayTo
{ {
} }
// first try to add a root node with a dlna namesapce // first try to add a root node with a dlna namespace.
try try
{ {
return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>") return XElement.Parse("<data xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + xml + "</data>")

View File

@ -60,7 +60,7 @@ namespace Emby.Naming.Subtitles
private string[] GetFlags(string path) private string[] GetFlags(string path)
{ {
// Note: the tags need be be surrounded be either a space ( ), hyphen -, dot . or underscore _. // Note: the tags need be surrounded be either a space ( ), hyphen -, dot . or underscore _.
var file = Path.GetFileName(path); var file = Path.GetFileName(path);

View File

@ -30,7 +30,7 @@ namespace Emby.Naming.Video
/// </summary> /// </summary>
/// <param name="files">List of related video files.</param> /// <param name="files">List of related video files.</param>
/// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param> /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
/// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files togeather when related.</returns> /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true) public IEnumerable<VideoInfo> Resolve(List<FileSystemMetadata> files, bool supportMultiVersion = true)
{ {
var videoResolver = new VideoResolver(_options); var videoResolver = new VideoResolver(_options);

View File

@ -98,7 +98,6 @@ using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Plugins.Tmdb;
using MediaBrowser.Providers.Subtitles; using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers; using MediaBrowser.XbmcMetadata.Providers;
@ -531,7 +530,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
ServiceCollection.AddSingleton(_fileSystemManager); ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TvdbClientManager>();
ServiceCollection.AddSingleton<TmdbClientManager>(); ServiceCollection.AddSingleton<TmdbClientManager>();
ServiceCollection.AddSingleton(NetManager); ServiceCollection.AddSingleton(NetManager);

View File

@ -634,7 +634,7 @@ namespace Emby.Server.Implementations.Channels
{ {
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray(); var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
if (query.ChannelIds.Length > 0) if (query.ChannelIds.Count > 0)
{ {
// Avoid implicitly captured closure // Avoid implicitly captured closure
var ids = query.ChannelIds; var ids = query.ChannelIds;

View File

@ -3611,12 +3611,12 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add($"type in ({inClause})"); whereClauses.Add($"type in ({inClause})");
} }
if (query.ChannelIds.Length == 1) if (query.ChannelIds.Count == 1)
{ {
whereClauses.Add("ChannelId=@ChannelId"); whereClauses.Add("ChannelId=@ChannelId");
statement?.TryBind("@ChannelId", query.ChannelIds[0].ToString("N", CultureInfo.InvariantCulture)); 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) + "'")); var inClause = string.Join(",", query.ChannelIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
whereClauses.Add($"ChannelId in ({inClause})"); whereClauses.Add($"ChannelId in ({inClause})");
@ -4076,7 +4076,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.GenreIds.Length > 0) if (query.GenreIds.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;
@ -4097,7 +4097,7 @@ namespace Emby.Server.Implementations.Data
whereClauses.Add(clause); whereClauses.Add(clause);
} }
if (query.Genres.Length > 0) if (query.Genres.Count > 0)
{ {
var clauses = new List<string>(); var clauses = new List<string>();
var index = 0; var index = 0;

View File

@ -1,61 +1,38 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Data.Events; using Jellyfin.Data.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices; using MediaBrowser.Model.Devices;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices namespace Emby.Server.Implementations.Devices
{ {
public class DeviceManager : IDeviceManager public class DeviceManager : IDeviceManager
{ {
private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo; 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, IUserManager userManager)
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
IServerConfigurationManager config,
IMemoryCache memoryCache)
{ {
_json = json;
_userManager = userManager; _userManager = userManager;
_config = config;
_memoryCache = memoryCache;
_authRepo = authRepo; _authRepo = authRepo;
} }
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{ {
var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json"); _capabilitiesMap[deviceId] = capabilities;
Directory.CreateDirectory(Path.GetDirectoryName(path));
lock (_capabilitiesSyncLock)
{
_memoryCache.Set(deviceId, capabilities);
_json.SerializeToFile(capabilities, path);
}
} }
public void UpdateDeviceOptions(string deviceId, DeviceOptions options) public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
@ -72,32 +49,12 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id) public ClientCapabilities GetCapabilities(string id)
{ {
if (_memoryCache.TryGetValue(id, out ClientCapabilities result)) return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
{ ? result
return result; : new ClientCapabilities();
}
lock (_capabilitiesSyncLock)
{
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{
return _json.DeserializeFromFile<ClientCapabilities>(path) ?? new ClientCapabilities();
}
catch
{
}
}
return new ClientCapabilities();
} }
public DeviceInfo GetDevice(string id) public DeviceInfo GetDevice(string id)
{
return GetDevice(id, true);
}
private DeviceInfo GetDevice(string id, bool includeCapabilities)
{ {
var session = _authRepo.Get(new AuthenticationInfoQuery 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) public bool CanAccessDevice(User user, string deviceId)
{ {
if (user == null) if (user == null)

View File

@ -1503,7 +1503,7 @@ namespace Emby.Server.Implementations.Library
{ {
if (query.AncestorIds.Length == 0 && if (query.AncestorIds.Length == 0 &&
query.ParentId.Equals(Guid.Empty) && query.ParentId.Equals(Guid.Empty) &&
query.ChannelIds.Length == 0 && query.ChannelIds.Count == 0 &&
query.TopParentIds.Length == 0 && query.TopParentIds.Length == 0 &&
string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) && string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&

View File

@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library
private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences) private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences)
{ {
// Give some preferance to external text subs for better performance // Give some preference to external text subs for better performance
return streams.Where(i => i.Type == type) return streams.Where(i => i.Type == type)
.OrderBy(i => .OrderBy(i =>
{ {

View File

@ -1635,7 +1635,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http))
{ {
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
} }
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper); return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);

View File

@ -8,7 +8,9 @@ using System.IO;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -25,6 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerApplicationPaths _appPaths; private readonly IServerApplicationPaths _appPaths;
private readonly IJsonSerializer _json; private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager;
private bool _hasExited; private bool _hasExited;
private Stream _logFileStream; private Stream _logFileStream;
@ -35,12 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ILogger logger, ILogger logger,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths, IServerApplicationPaths appPaths,
IJsonSerializer json) IJsonSerializer json,
IServerConfigurationManager serverConfigurationManager)
{ {
_logger = logger; _logger = logger;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_appPaths = appPaths; _appPaths = appPaths;
_json = json; _json = json;
_serverConfigurationManager = serverConfigurationManager;
} }
private static bool CopySubtitles => false; private static bool CopySubtitles => false;
@ -179,15 +184,17 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var outputParam = string.Empty; var outputParam = string.Empty;
var threads = EncodingHelper.GetNumberOfThreads(null, _serverConfigurationManager.GetEncodingOptions(), null);
var commandLineArgs = string.Format( var commandLineArgs = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"-i \"{0}\" {2} -map_metadata -1 -threads 0 {3}{4}{5} -y \"{1}\"", "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
inputTempFile, inputTempFile,
targetFile, targetFile,
videoArgs, videoArgs,
GetAudioArgs(mediaSource), GetAudioArgs(mediaSource),
subtitleArgs, subtitleArgs,
outputParam); outputParam,
threads);
return inputModifier + " " + commandLineArgs; return inputModifier + " " + commandLineArgs;
} }

View File

@ -261,7 +261,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
Id = newID, Id = newID,
StartDate = startAt, StartDate = startAt,
EndDate = endAt, EndDate = endAt,
Name = details.titles[0].title120 ?? "Unkown", Name = details.titles[0].title120 ?? "Unknown",
OfficialRating = null, OfficialRating = null,
CommunityRating = null, CommunityRating = null,
EpisodeTitle = episodeTitle, EpisodeTitle = episodeTitle,

View File

@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrWhiteSpace(numberString)) if (string.IsNullOrWhiteSpace(numberString))
{ {
// Using this as a fallback now as this leads to Problems with channels like "5 USA" // Using this as a fallback now as this leads to Problems with channels like "5 USA"
// where 5 isnt ment to be the channel number // where 5 isn't ment to be the channel number
// Check for channel number with the format from SatIp // Check for channel number with the format from SatIp
// #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz

View File

@ -113,5 +113,10 @@
"TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή", "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
"TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.", "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
"TaskUpdatePlugins": "Ενημέρωση Προσθηκών", "TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
"TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας." "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας.",
"TaskCleanActivityLogDescription": "Διαγράφει καταχωρήσεις απο το αρχείο καταγραφής δραστηριοτήτων παλαιότερες από την ηλικία που έχει διαμορφωθεί.",
"TaskCleanActivityLog": "Καθαρό Αρχείο Καταγραφής Δραστηριοτήτων",
"Undefined": "Απροσδιόριστο",
"Forced": "Εξαναγκασμένο",
"Default": "Προεπιλογή"
} }

View File

@ -113,5 +113,10 @@
"TaskRefreshChannels": "רענן ערוץ", "TaskRefreshChannels": "רענן ערוץ",
"TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.", "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
"TaskCleanTranscode": "נקה תקיית Transcode", "TaskCleanTranscode": "נקה תקיית Transcode",
"TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי." "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי.",
"TaskCleanActivityLogDescription": "מחק רשומת פעילות הישנה יותר מהגיל המוגדר.",
"TaskCleanActivityLog": "נקה רשומת פעילות",
"Undefined": "לא מוגדר",
"Forced": "כפוי",
"Default": "ברירת מחדל"
} }

View File

@ -115,5 +115,8 @@
"TaskRefreshChannels": "Csatornák frissítése", "TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.", "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.", "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"
} }

View File

@ -113,5 +113,10 @@
"TasksChannelsCategory": "Kanały internetowe", "TasksChannelsCategory": "Kanały internetowe",
"TasksApplicationCategory": "Aplikacja", "TasksApplicationCategory": "Aplikacja",
"TasksLibraryCategory": "Biblioteka", "TasksLibraryCategory": "Biblioteka",
"TasksMaintenanceCategory": "Konserwacja" "TasksMaintenanceCategory": "Konserwacja",
"TaskCleanActivityLogDescription": "Usuwa wpisy dziennika aktywności starsze niż skonfigurowany wiek.",
"TaskCleanActivityLog": "Czyść dziennik aktywności",
"Undefined": "Nieustalony",
"Forced": "Wymuszony",
"Default": "Domyślne"
} }

View File

@ -150,7 +150,7 @@ namespace Emby.Server.Implementations.Playlists
await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None) await playlist.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { ForceSave = true }, CancellationToken.None)
.ConfigureAwait(false); .ConfigureAwait(false);
if (options.ItemIdList.Length > 0) if (options.ItemIdList.Count > 0)
{ {
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) 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); 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); 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 // Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist

View File

@ -136,7 +136,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{ {
var type = scheduledTask.ScheduledTask.GetType(); var type = scheduledTask.ScheduledTask.GetType();
_logger.LogInformation("Queueing task {0}", type.Name); _logger.LogInformation("Queuing task {0}", type.Name);
lock (_taskQueue) lock (_taskQueue)
{ {
@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{ {
var type = task.ScheduledTask.GetType(); var type = task.ScheduledTask.GetType();
_logger.LogInformation("Queueing task {0}", type.Name); _logger.LogInformation("Queuing task {0}", type.Name);
lock (_taskQueue) lock (_taskQueue)
{ {

View File

@ -58,8 +58,7 @@ namespace Emby.Server.Implementations.Session
/// <summary> /// <summary>
/// The active connections. /// The active connections.
/// </summary> /// </summary>
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new (StringComparer.OrdinalIgnoreCase);
new ConcurrentDictionary<string, SessionInfo>(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer; private Timer _idleTimer;
@ -196,7 +195,7 @@ namespace Emby.Server.Implementations.Session
{ {
if (!string.IsNullOrEmpty(info.DeviceId)) if (!string.IsNullOrEmpty(info.DeviceId))
{ {
var capabilities = GetSavedCapabilities(info.DeviceId); var capabilities = _deviceManager.GetCapabilities(info.DeviceId);
if (capabilities != null) if (capabilities != null)
{ {
@ -1677,27 +1676,10 @@ namespace Emby.Server.Implementations.Session
SessionInfo = session SessionInfo = session
}); });
try _deviceManager.SaveCapabilities(session.DeviceId, capabilities);
{
SaveCapabilities(session.DeviceId, capabilities);
}
catch (Exception ex)
{
_logger.LogError("Error saving device capabilities", ex);
}
} }
} }
private ClientCapabilities GetSavedCapabilities(string deviceId)
{
return _deviceManager.GetCapabilities(deviceId);
}
private void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
_deviceManager.SaveCapabilities(deviceId, capabilities);
}
/// <summary> /// <summary>
/// Converts a BaseItem to a BaseItemInfo. /// Converts a BaseItem to a BaseItemInfo.
/// </summary> /// </summary>

View File

@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.SyncPlay
new Dictionary<Guid, ISyncPlayController>(); new Dictionary<Guid, ISyncPlayController>();
/// <summary> /// <summary>
/// Lock used for accesing any group. /// Lock used for accessing any group.
/// </summary> /// </summary>
private readonly object _groupsLock = new object(); private readonly object _groupsLock = new object();

View File

@ -89,24 +89,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -131,30 +131,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); 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) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
@ -174,9 +170,9 @@ namespace Jellyfin.Api.Controllers
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -230,7 +226,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;
@ -297,24 +293,24 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
@ -339,30 +335,26 @@ namespace Jellyfin.Api.Controllers
parentItem = string.IsNullOrEmpty(parentId) ? _libraryManager.RootFolder : _libraryManager.GetItemById(parentId); 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) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
NameLessThan = nameLessThan, NameLessThan = nameLessThan,
NameStartsWith = nameStartsWith, NameStartsWith = nameStartsWith,
NameStartsWithOrGreater = nameStartsWithOrGreater, NameStartsWithOrGreater = nameStartsWithOrGreater,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
@ -382,9 +374,9 @@ namespace Jellyfin.Api.Controllers
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -438,7 +430,7 @@ namespace Jellyfin.Api.Controllers
var (baseItem, itemCounts) = i; var (baseItem, itemCounts) = i;
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user); var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
if (!string.IsNullOrWhiteSpace(includeItemTypes)) if (includeItemTypes.Length != 0)
{ {
dto.ChildCount = itemCounts.ItemCount; dto.ChildCount = itemCounts.ItemCount;
dto.ProgramCount = itemCounts.ProgramCount; dto.ProgramCount = itemCounts.ProgramCount;

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -42,7 +42,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>

View File

@ -198,7 +198,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? channelIds) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
{ {
var user = userId.HasValue && !userId.Equals(Guid.Empty) var user = userId.HasValue && !userId.Equals(Guid.Empty)
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
@ -208,11 +208,7 @@ namespace Jellyfin.Api.Controllers
{ {
Limit = limit, Limit = limit,
StartIndex = startIndex, StartIndex = startIndex,
ChannelIds = (channelIds ?? string.Empty) ChannelIds = channelIds,
.Split(',')
.Where(i => !string.IsNullOrWhiteSpace(i))
.Select(i => new Guid(i))
.ToArray(),
DtoOptions = new DtoOptions { Fields = fields } DtoOptions = new DtoOptions { Fields = fields }
}; };

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
@ -54,7 +55,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<CollectionCreationResult>> CreateCollection( public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
[FromQuery] string? name, [FromQuery] string? name,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery] bool isLocked = false) [FromQuery] bool isLocked = false)
{ {
@ -65,7 +66,7 @@ namespace Jellyfin.Api.Controllers
IsLocked = isLocked, IsLocked = isLocked,
Name = name, Name = name,
ParentId = parentId, ParentId = parentId,
ItemIdList = RequestHelpers.Split(ids, ',', true), ItemIdList = ids,
UserIds = new[] { userId } UserIds = new[] { userId }
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -88,9 +89,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("{collectionId}/Items")] [HttpPost("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [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(); return NoContent();
} }
@ -103,9 +106,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns> /// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpDelete("{collectionId}/Items")] [HttpDelete("{collectionId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [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(); return NoContent();
} }
} }

View File

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public class DynamicHlsController : BaseJellyfinApiController public class DynamicHlsController : BaseJellyfinApiController
{ {
private const string DefaultEncoderPreset = "veryfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILogger<DynamicHlsController> _logger; private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper; private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly EncodingOptions _encodingOptions;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class. /// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
ILogger<DynamicHlsController> logger, ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper) DynamicHlsHelper dynamicHlsHelper)
{ {
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
_transcodingJobHelper = transcodingJobHelper; _transcodingJobHelper = transcodingJobHelper;
_logger = logger; _logger = logger;
_dynamicHlsHelper = dynamicHlsHelper; _dynamicHlsHelper = dynamicHlsHelper;
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
} }
/// <summary> /// <summary>
@ -120,7 +124,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -149,7 +153,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -272,7 +276,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -285,7 +289,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -315,7 +319,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -439,7 +443,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
}; };
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -452,7 +456,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -481,7 +485,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -615,7 +619,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -645,7 +649,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -812,7 +816,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -953,7 +957,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -983,7 +987,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager, _dlnaManager,
_deviceManager, _deviceManager,
_transcodingJobHelper, _transcodingJobHelper,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1137,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
var segmentLengths = GetSegmentLengths(state); 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(); var builder = new StringBuilder();
builder.AppendLine("#EXTM3U") builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD") .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
.AppendLine("#EXT-X-VERSION:3") .Append("#EXT-X-VERSION:")
.Append(hlsVersion)
.AppendLine()
.Append("#EXT-X-TARGETDURATION:") .Append("#EXT-X-TARGETDURATION:")
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength)) .Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
.AppendLine() .AppendLine()
@ -1151,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer); var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
var queryString = Request.QueryString; 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) foreach (var length in segmentLengths)
{ {
builder.Append("#EXTINF:") builder.Append("#EXTINF:")
@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager, _dlnaManager,
_deviceManager, _deviceManager,
_transcodingJobHelper, _transcodingJobHelper,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1208,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
if (System.IO.File.Exists(segmentPath)) 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); _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); 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)) if (System.IO.File.Exists(segmentPath))
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release(); transcodingLock.Release();
released = true; released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@ -1233,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension); var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength; 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"); _logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true; startTranscoding = true;
@ -1265,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId); streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
state.WaitForPath = segmentPath; state.WaitForPath = segmentPath;
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodingJobHelper.StartFfMpeg(
state, state,
playlistPath, playlistPath,
GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId), GetCommandLineArguments(playlistPath, state, true, segmentId),
Request, Request,
_transcodingJobType, TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false); cancellationTokenSource).ConfigureAwait(false);
} }
catch catch
@ -1284,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
} }
else else
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType); job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler != null) if (job?.TranscodingThrottler != null)
{ {
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@ -1301,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
} }
_logger.LogDebug("returning {0} [general case]", segmentPath); _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); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
} }
@ -1325,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
return result.ToArray(); 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 videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames) if (state.BaseRequest.BreakOnNonKeyFrames)
{ {
@ -1341,36 +1369,57 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false; state.BaseRequest.BreakOnNonKeyFrames = false;
} }
var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg // If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0"; var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty; 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 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 = outputExtension.TrimStart('.');
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{ {
segmentFormat = "mpegts"; 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 segmentFormat = "fmp4" + outputFmp4HeaderArg;
? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) }
else
{
_logger.LogError("Invalid HLS segment container: " + segmentFormat);
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
: "128"; : "128";
return string.Format( return string.Format(
CultureInfo.InvariantCulture, 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, inputModifier,
_encodingHelper.GetInputArgument(state, encodingOptions), _encodingHelper.GetInputArgument(state, _encodingOptions),
threads, threads,
mapArgs, mapArgs,
GetVideoArguments(state, encodingOptions, startNumber), GetVideoArguments(state, startNumber),
GetAudioArguments(state, encodingOptions), GetAudioArguments(state),
maxMuxingQueueSize, maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture), state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat, segmentFormat,
@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
outputPath).Trim(); 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); var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo) if (!state.IsOutputVideo)
{ {
if (EncodingHelper.IsCopyCodec(audioCodec)) 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) 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) 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) if (state.OutputAudioSampleRate.HasValue)
{ {
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture)); audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
audioTranscodeParams.Add("-vn"); audioTranscodeParams += " -vn";
return string.Join(' ', audioTranscodeParams); return audioTranscodeParams;
} }
if (EncodingHelper.IsCopyCodec(audioCodec)) 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)) 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; var args = "-codec:a:0 " + audioCodec;
@ -1446,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true); args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args; 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) if (!state.IsOutputVideo)
{ {
return string.Empty; return string.Empty;
} }
var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions); var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var args = "-codec:v:0 " + codec; 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) // if (state.EnableMpegtsM2TsMode)
// { // {
// args += " -mpegts_m2ts_mode 1"; // 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 (EncodingHelper.IsCopyCodec(codec))
{ {
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase)) 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)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;
} }
} }
args += " -start_at_zero";
// args += " -flags -global_header"; // args += " -flags -global_header";
} }
else else
{ {
var gopArg = string.Empty; args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
var keyFrameArg = string.Format(
CultureInfo.InvariantCulture,
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
startNumber * state.SegmentLength,
state.SegmentLength);
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, args += " -bf 0";
// 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 += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=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; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// This is for graphical subs
if (hasGraphicalSubs) 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 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, // -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) private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{ {
var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType); var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited) if (job == null || job.HasExited)
{ {

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Audio;
@ -50,8 +51,8 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy( public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? mediaTypes) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
{ {
var parentItem = string.IsNullOrEmpty(parentId) var parentItem = string.IsNullOrEmpty(parentId)
? null ? null
@ -61,10 +62,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; parentItem = null;
} }
@ -78,8 +80,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery var query = new InternalItemsQuery
{ {
User = user, User = user,
MediaTypes = (mediaTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), MediaTypes = mediaTypes,
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries), IncludeItemTypes = includeItemTypes,
Recursive = true, Recursive = true,
EnableTotalRecordCount = false, EnableTotalRecordCount = false,
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
@ -139,7 +141,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryFilters> GetQueryFilters( public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isAiring, [FromQuery] bool? isAiring,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSports, [FromQuery] bool? isSports,
@ -156,10 +158,11 @@ namespace Jellyfin.Api.Controllers
? _userManager.GetUserById(userId.Value) ? _userManager.GetUserById(userId.Value)
: null; : null;
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase)) || string.Equals(includeItemTypes[0], nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes[0], "Program", StringComparison.OrdinalIgnoreCase)))
{ {
parentItem = null; parentItem = null;
} }
@ -167,8 +170,7 @@ namespace Jellyfin.Api.Controllers
var filters = new QueryFilters(); var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user) var genreQuery = new InternalItemsQuery(user)
{ {
IncludeItemTypes = IncludeItemTypes = includeItemTypes,
(includeItemTypes ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries),
DtoOptions = new DtoOptions DtoOptions = new DtoOptions
{ {
Fields = Array.Empty<ItemFields>(), Fields = Array.Empty<ItemFields>(),
@ -192,10 +194,11 @@ namespace Jellyfin.Api.Controllers
genreQuery.Parent = parentItem; genreQuery.Parent = parentItem;
} }
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase) && (string.Equals(includeItemTypes[0], nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase) || string.Equals(includeItemTypes[0], nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Audio), 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 filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{ {

View File

@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -133,7 +133,7 @@ namespace Jellyfin.Api.Controllers
result = _libraryManager.GetGenres(query); result = _libraryManager.GetGenres(query);
} }
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View File

@ -112,11 +112,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="segmentId">The segment id.</param> /// <param name="segmentId">The segment id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <response code="200">Hls video segment returned.</response> /// <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> /// <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 // Can't require authentication just yet due to seeing some requests come from Chrome without full query string
// [Authenticated] // [Authenticated]
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")] [HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesVideoFile] [ProducesVideoFile]
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsVideoSegmentLegacy( public ActionResult GetHlsVideoSegmentLegacy(
@ -132,13 +134,25 @@ namespace Jellyfin.Api.Controllers
var normalizedPlaylistId = playlistId; var normalizedPlaylistId = playlistId;
var playlistPath = _fileSystem.GetFilePaths(transcodeFolderPath) var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
.FirstOrDefault(i => // Add . to start of segment container for future use.
string.Equals(Path.GetExtension(i), ".m3u8", StringComparison.OrdinalIgnoreCase) segmentContainer = segmentContainer.Insert(0, ".");
&& i.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1) string? playlistPath = null;
?? throw new ResourceNotFoundException($"Provided path ({transcodeFolderPath}) is not valid."); 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) private ActionResult GetFileResult(string path, string playlistPath)

View File

@ -1,4 +1,4 @@
using System; using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</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="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="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="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param> /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> /// <param name="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="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param> /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@ -87,42 +87,42 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id 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="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="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="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="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="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="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</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="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> /// <param name="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="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="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param> /// <param name="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="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param> /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param> /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param> /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param> /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</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="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="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="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="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="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param> /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param> /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> /// <param name="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="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="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="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param> /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param> /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="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="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@ -133,12 +133,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width 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="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="is3D">Optional filter by items that are 3D, or not.</param>
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param> /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="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="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="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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param> /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the items.</returns>
@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] string? excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
@ -181,34 +181,34 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? artists, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] artists,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] string? artistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery] string? albumArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery] string? contributingArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery] string? albums, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] albums,
[FromQuery] string? albumIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] string? videoTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating, [FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked, [FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder, [FromQuery] bool? isPlaceHolder,
@ -219,12 +219,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] bool? is3D, [FromQuery] bool? is3D,
[FromQuery] string? seriesStatus, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan, [FromQuery] string? nameLessThan,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
@ -238,8 +238,9 @@ namespace Jellyfin.Api.Controllers
.AddClientFields(Request) .AddClientFields(Request)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
if (string.Equals(includeItemTypes, "Playlist", StringComparison.OrdinalIgnoreCase) if (includeItemTypes.Length == 1
|| string.Equals(includeItemTypes, "BoxSet", StringComparison.OrdinalIgnoreCase)) && (includeItemTypes[0].Equals("Playlist", StringComparison.OrdinalIgnoreCase)
|| includeItemTypes[0].Equals("BoxSet", StringComparison.OrdinalIgnoreCase)))
{ {
parentId = null; parentId = null;
} }
@ -262,7 +263,7 @@ namespace Jellyfin.Api.Controllers
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase)) && string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
{ {
recursive = true; recursive = true;
includeItemTypes = "Playlist"; includeItemTypes = new[] { "Playlist" };
} }
bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
@ -291,14 +292,14 @@ namespace Jellyfin.Api.Controllers
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}."); return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
} }
if ((recursive.HasValue && recursive.Value) || !string.IsNullOrEmpty(ids) || !(item is UserRootFolder)) if ((recursive.HasValue && recursive.Value) || ids.Length != 0 || !(item is UserRootFolder))
{ {
var query = new InternalItemsQuery(user!) var query = new InternalItemsQuery(user!)
{ {
IsPlayed = isPlayed, IsPlayed = isPlayed,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
Recursive = recursive ?? false, Recursive = recursive ?? false,
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder), OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -330,28 +331,28 @@ namespace Jellyfin.Api.Controllers
HasTrailer = hasTrailer, HasTrailer = hasTrailer,
IsHD = isHd, IsHD = isHd,
Is4K = is4K, Is4K = is4K,
Tags = RequestHelpers.Split(tags, '|', true), Tags = tags,
OfficialRatings = RequestHelpers.Split(officialRatings, '|', true), OfficialRatings = officialRatings,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
ArtistIds = RequestHelpers.GetGuids(artistIds), ArtistIds = artistIds,
AlbumArtistIds = RequestHelpers.GetGuids(albumArtistIds), AlbumArtistIds = albumArtistIds,
ContributingArtistIds = RequestHelpers.GetGuids(contributingArtistIds), ContributingArtistIds = contributingArtistIds,
GenreIds = RequestHelpers.GetGuids(genreIds), GenreIds = genreIds,
StudioIds = RequestHelpers.GetGuids(studioIds), StudioIds = studioIds,
Person = person, Person = person,
PersonIds = RequestHelpers.GetGuids(personIds), PersonIds = personIds,
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
Years = RequestHelpers.Split(years, ',', true).Select(int.Parse).ToArray(), Years = years,
ImageTypes = imageTypes, ImageTypes = imageTypes,
VideoTypes = RequestHelpers.Split(videoTypes, ',', true).Select(v => Enum.Parse<VideoType>(v, true)).ToArray(), VideoTypes = videoTypes,
AdjacentTo = adjacentTo, AdjacentTo = adjacentTo,
ItemIds = RequestHelpers.GetGuids(ids), ItemIds = ids,
MinCommunityRating = minCommunityRating, MinCommunityRating = minCommunityRating,
MinCriticRating = minCriticRating, MinCriticRating = minCriticRating,
ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId), ParentId = string.IsNullOrWhiteSpace(parentId) ? Guid.Empty : new Guid(parentId),
ParentIndexNumber = parentIndexNumber, ParentIndexNumber = parentIndexNumber,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
ExcludeItemIds = RequestHelpers.GetGuids(excludeItemIds), ExcludeItemIds = excludeItemIds,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
SearchTerm = searchTerm, SearchTerm = searchTerm,
MinDateLastSaved = minDateLastSaved?.ToUniversalTime(), MinDateLastSaved = minDateLastSaved?.ToUniversalTime(),
@ -360,7 +361,7 @@ namespace Jellyfin.Api.Controllers
MaxPremiereDate = maxPremiereDate?.ToUniversalTime(), MaxPremiereDate = maxPremiereDate?.ToUniversalTime(),
}; };
if (!string.IsNullOrWhiteSpace(ids) || !string.IsNullOrWhiteSpace(searchTerm)) if (ids.Length != 0 || !string.IsNullOrWhiteSpace(searchTerm))
{ {
query.CollapseBoxSetItems = false; query.CollapseBoxSetItems = false;
} }
@ -400,9 +401,9 @@ namespace Jellyfin.Api.Controllers
} }
// Filter by Series Status // 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 // ExcludeLocationTypes
@ -411,13 +412,9 @@ namespace Jellyfin.Api.Controllers
query.IsVirtualItem = false; query.IsVirtualItem = false;
} }
if (!string.IsNullOrEmpty(locationTypes)) if (locationTypes.Length > 0 && locationTypes.Length < 4)
{ {
var requestedLocationTypes = locationTypes.Split(','); query.IsVirtualItem = locationTypes.Contains(LocationType.Virtual);
if (requestedLocationTypes.Length > 0 && requestedLocationTypes.Length < 4)
{
query.IsVirtualItem = requestedLocationTypes.Contains(LocationType.Virtual.ToString());
}
} }
// Min official rating // Min official rating
@ -433,9 +430,9 @@ namespace Jellyfin.Api.Controllers
} }
// Artists // Artists
if (!string.IsNullOrEmpty(artists)) if (artists.Length != 0)
{ {
query.ArtistIds = artists.Split('|').Select(i => query.ArtistIds = artists.Select(i =>
{ {
try try
{ {
@ -449,29 +446,29 @@ namespace Jellyfin.Api.Controllers
} }
// ExcludeArtistIds // 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 // 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 }); return _libraryManager.GetItemIds(new InternalItemsQuery { IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = i, Limit = 1 });
}).ToArray(); }).ToArray();
} }
// Studios // Studios
if (!string.IsNullOrEmpty(studios)) if (studios.Length != 0)
{ {
query.StudioIds = studios.Split('|').Select(i => query.StudioIds = studios.Select(i =>
{ {
try try
{ {
@ -513,13 +510,13 @@ namespace Jellyfin.Api.Controllers
/// <param name="limit">The item limit.</param> /// <param name="limit">The item limit.</param>
/// <param name="searchTerm">The search term.</param> /// <param name="searchTerm">The search term.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param> /// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional. Include user data.</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="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="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on the item type. This allows multiple, comma delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional. Include image information in output.</param> /// <param name="enableImages">Optional. Include image information in output.</param>
/// <response code="200">Items returned.</response> /// <response code="200">Items returned.</response>
@ -533,12 +530,12 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
@ -569,13 +566,13 @@ namespace Jellyfin.Api.Controllers
ParentId = parentIdGuid, ParentId = parentIdGuid,
Recursive = true, Recursive = true,
DtoOptions = dtoOptions, DtoOptions = dtoOptions,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
IsVirtualItem = false, IsVirtualItem = false,
CollapseBoxSetItems = false, CollapseBoxSetItems = false,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
AncestorIds = ancestorIds, AncestorIds = ancestorIds,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
SearchTerm = searchTerm SearchTerm = searchTerm
}); });

View File

@ -362,15 +362,14 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [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(); return NoContent();
} }
var itemIds = RequestHelpers.Split(ids, ',', true); foreach (var i in ids)
foreach (var i in itemIds)
{ {
var item = _libraryManager.GetItemById(i); var item = _libraryManager.GetItemById(i);
var auth = _authContext.GetAuthorizationInfo(Request); var auth = _authContext.GetAuthorizationInfo(Request);
@ -691,7 +690,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems( public ActionResult<QueryResult<BaseItemDto>> GetSimilarItems(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
@ -753,9 +752,9 @@ namespace Jellyfin.Api.Controllers
}; };
// ExcludeArtistIds // ExcludeArtistIds
if (!string.IsNullOrEmpty(excludeArtistIds)) if (excludeArtistIds.Length != 0)
{ {
query.ExcludeArtistIds = RequestHelpers.GetGuids(excludeArtistIds); query.ExcludeArtistIds = excludeArtistIds;
} }
List<BaseItem> itemsResult = _libraryManager.GetItemList(query); List<BaseItem> itemsResult = _libraryManager.GetItemList(query);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] string? sortBy, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
[FromQuery] SortOrder? sortOrder, [FromQuery] SortOrder? sortOrder,
[FromQuery] bool enableFavoriteSorting = false, [FromQuery] bool enableFavoriteSorting = false,
[FromQuery] bool addCurrentProgram = true) [FromQuery] bool addCurrentProgram = true)
@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SortBy = RequestHelpers.Split(sortBy, ',', true), SortBy = sortBy,
SortOrder = sortOrder ?? SortOrder.Ascending, SortOrder = sortOrder ?? SortOrder.Ascending,
AddCurrentProgram = addCurrentProgram AddCurrentProgram = addCurrentProgram
}, },
@ -539,7 +539,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms( public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLiveTvPrograms(
[FromQuery] string? channelIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] DateTime? minStartDate, [FromQuery] DateTime? minStartDate,
[FromQuery] bool? hasAired, [FromQuery] bool? hasAired,
@ -556,8 +556,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -573,8 +573,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(channelIds, ',', true) ChannelIds = channelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = hasAired, HasAired = hasAired,
IsAiring = isAiring, IsAiring = isAiring,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
@ -591,8 +590,8 @@ namespace Jellyfin.Api.Controllers
IsKids = isKids, IsKids = isKids,
IsSports = isSports, IsSports = isSports,
SeriesTimerId = seriesTimerId, SeriesTimerId = seriesTimerId,
Genres = RequestHelpers.Split(genres, '|', true), Genres = genres,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty)) if (librarySeriesId != null && !librarySeriesId.Equals(Guid.Empty))
@ -628,8 +627,7 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ChannelIds = RequestHelpers.Split(body.ChannelIds, ',', true) ChannelIds = body.ChannelIds,
.Select(i => new Guid(i)).ToArray(),
HasAired = body.HasAired, HasAired = body.HasAired,
IsAiring = body.IsAiring, IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount, EnableTotalRecordCount = body.EnableTotalRecordCount,
@ -646,8 +644,8 @@ namespace Jellyfin.Api.Controllers
IsKids = body.IsKids, IsKids = body.IsKids,
IsSports = body.IsSports, IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId, SeriesTimerId = body.SeriesTimerId,
Genres = RequestHelpers.Split(body.Genres, '|', true), Genres = body.Genres,
GenreIds = RequestHelpers.GetGuids(body.GenreIds) GenreIds = body.GenreIds
}; };
if (!body.LibrarySeriesId.Equals(Guid.Empty)) if (!body.LibrarySeriesId.Equals(Guid.Empty))
@ -703,7 +701,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] bool enableTotalRecordCount = true) [FromQuery] bool enableTotalRecordCount = true)
@ -723,7 +721,7 @@ namespace Jellyfin.Api.Controllers
IsNews = isNews, IsNews = isNews,
IsSports = isSports, IsSports = isSports,
EnableTotalRecordCount = enableTotalRecordCount, EnableTotalRecordCount = enableTotalRecordCount,
GenreIds = RequestHelpers.GetGuids(genreIds) GenreIds = genreIds
}; };
var dtoOptions = new DtoOptions { Fields = fields } var dtoOptions = new DtoOptions { Fields = fields }
@ -1155,7 +1153,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="newDevicesOnly">Only discover new tuners.</param> /// <param name="newDevicesOnly">Only discover new tuners.</param>
/// <response code="200">Tuners returned.</response> /// <response code="200">Tuners returned.</response>
/// <returns>An <see cref="OkResult"/> containing the tuners.</returns> /// <returns>An <see cref="OkResult"/> containing the tuners.</returns>
[HttpGet("Tuners/Discvover")] [HttpGet("Tuners/Discvover", Name = "DiscvoverTuners")]
[HttpGet("Tuners/Discover")]
[Authorize(Policy = Policies.DefaultAuthorization)] [Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)

View File

@ -74,8 +74,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
@ -96,8 +96,8 @@ namespace Jellyfin.Api.Controllers
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetMusicGenres(query); var result = _libraryManager.GetMusicGenres(query);
var shouldIncludeItemTypes = !string.IsNullOrWhiteSpace(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View File

@ -77,8 +77,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? excludePersonTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludePersonTypes,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? appearsInItemId, [FromQuery] string? appearsInItemId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
@ -97,8 +97,8 @@ namespace Jellyfin.Api.Controllers
var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite); var isFavoriteInFilters = filters.Any(f => f == ItemFilter.IsFavorite);
var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery var peopleItems = _libraryManager.GetPeopleItems(new InternalPeopleQuery
{ {
PersonTypes = RequestHelpers.Split(personTypes, ',', true), PersonTypes = personTypes,
ExcludePersonTypes = RequestHelpers.Split(excludePersonTypes, ',', true), ExcludePersonTypes = excludePersonTypes,
NameContains = searchTerm, NameContains = searchTerm,
User = user, User = user,
IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite, IsFavorite = !isFavorite.HasValue && isFavoriteInFilters ? true : isFavorite,

View File

@ -63,11 +63,10 @@ namespace Jellyfin.Api.Controllers
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist( public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
[FromBody, Required] CreatePlaylistDto createPlaylistRequest) [FromBody, Required] CreatePlaylistDto createPlaylistRequest)
{ {
Guid[] idGuidArray = RequestHelpers.GetGuids(createPlaylistRequest.Ids);
var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest var result = await _playlistManager.CreatePlaylist(new PlaylistCreationRequest
{ {
Name = createPlaylistRequest.Name, Name = createPlaylistRequest.Name,
ItemIdList = idGuidArray, ItemIdList = createPlaylistRequest.Ids,
UserId = createPlaylistRequest.UserId, UserId = createPlaylistRequest.UserId,
MediaType = createPlaylistRequest.MediaType MediaType = createPlaylistRequest.MediaType
}).ConfigureAwait(false); }).ConfigureAwait(false);
@ -87,10 +86,10 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddToPlaylist( public async Task<ActionResult> AddToPlaylist(
[FromRoute, Required] Guid playlistId, [FromRoute, Required] Guid playlistId,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] Guid? userId) [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(); return NoContent();
} }
@ -122,9 +121,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>An <see cref="NoContentResult"/> on success.</returns> /// <returns>An <see cref="NoContentResult"/> on success.</returns>
[HttpDelete("{playlistId}/Items")] [HttpDelete("{playlistId}/Items")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [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(); return NoContent();
} }

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -82,9 +83,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
[FromQuery, Required] string searchTerm, [FromQuery, Required] string searchTerm,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery] bool? isMovie, [FromQuery] bool? isMovie,
[FromQuery] bool? isSeries, [FromQuery] bool? isSeries,
@ -108,9 +109,9 @@ namespace Jellyfin.Api.Controllers
IncludeStudios = includeStudios, IncludeStudios = includeStudios,
StartIndex = startIndex, StartIndex = startIndex,
UserId = userId ?? Guid.Empty, UserId = userId ?? Guid.Empty,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
ExcludeItemTypes = RequestHelpers.Split(excludeItemTypes, ',', true), ExcludeItemTypes = excludeItemTypes,
MediaTypes = RequestHelpers.Split(mediaTypes, ',', true), MediaTypes = mediaTypes,
ParentId = parentId, ParentId = parentId,
IsKids = isKids, IsKids = isKids,

View File

@ -160,12 +160,12 @@ namespace Jellyfin.Api.Controllers
public ActionResult Play( public ActionResult Play(
[FromRoute, Required] string sessionId, [FromRoute, Required] string sessionId,
[FromQuery, Required] PlayCommand playCommand, [FromQuery, Required] PlayCommand playCommand,
[FromQuery, Required] string itemIds, [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
[FromQuery] long? startPositionTicks) [FromQuery] long? startPositionTicks)
{ {
var playRequest = new PlayRequest var playRequest = new PlayRequest
{ {
ItemIds = RequestHelpers.GetGuids(itemIds), ItemIds = itemIds,
StartPositionTicks = startPositionTicks, StartPositionTicks = startPositionTicks,
PlayCommand = playCommand PlayCommand = playCommand
}; };
@ -378,7 +378,7 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PostCapabilities( public ActionResult PostCapabilities(
[FromQuery] string? id, [FromQuery] string? id,
[FromQuery] string? playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false, [FromQuery] bool supportsSync = false,
@ -391,7 +391,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.ReportCapabilities(id, new ClientCapabilities _sessionManager.ReportCapabilities(id, new ClientCapabilities
{ {
PlayableMediaTypes = RequestHelpers.Split(playableMediaTypes, ',', true), PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands, SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl, SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync, SupportsSync = supportsSync,

View File

@ -73,8 +73,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? searchTerm, [FromQuery] string? searchTerm,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -94,13 +94,10 @@ namespace Jellyfin.Api.Controllers
var parentItem = _libraryManager.GetParentItem(parentId, userId); var parentItem = _libraryManager.GetParentItem(parentId, userId);
var excludeItemTypesArr = RequestHelpers.Split(excludeItemTypes, ',', true);
var includeItemTypesArr = RequestHelpers.Split(includeItemTypes, ',', true);
var query = new InternalItemsQuery(user) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,
IsFavorite = isFavorite, IsFavorite = isFavorite,
@ -125,7 +122,7 @@ namespace Jellyfin.Api.Controllers
} }
var result = _libraryManager.GetStudios(query); var result = _libraryManager.GetStudios(query);
var shouldIncludeItemTypes = !string.IsNullOrEmpty(includeItemTypes); var shouldIncludeItemTypes = includeItemTypes.Length != 0;
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user); return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
} }

View File

@ -4,6 +4,7 @@ using System.Linq;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -58,8 +59,8 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryResult<BaseItemDto>> GetSuggestions( public ActionResult<QueryResult<BaseItemDto>> GetSuggestions(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] string? mediaType, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaType,
[FromQuery] string? type, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] type,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool enableTotalRecordCount = false) [FromQuery] bool enableTotalRecordCount = false)
@ -70,8 +71,8 @@ namespace Jellyfin.Api.Controllers
var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user) var result = _libraryManager.GetItemsResult(new InternalItemsQuery(user)
{ {
OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(), OrderBy = new[] { ItemSortBy.Random }.Select(i => new ValueTuple<string, SortOrder>(i, SortOrder.Descending)).ToArray(),
MediaTypes = RequestHelpers.Split(mediaType!, ',', true), MediaTypes = mediaType,
IncludeItemTypes = RequestHelpers.Split(type!, ',', true), IncludeItemTypes = type,
IsVirtualItem = false, IsVirtualItem = false,
StartIndex = startIndex, StartIndex = startIndex,
Limit = limit, Limit = limit,

View File

@ -1,4 +1,4 @@
using System; using System;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
@ -42,8 +42,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasParentalRating">Optional filter by items that have or do not have a parental rating.</param> /// <param name="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="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="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="locationTypes">Optional. If specified, results will be filtered based on LocationType. This allows multiple, comma delimited.</param>
/// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimeted.</param> /// <param name="excludeLocationTypes">Optional. If specified, results will be filtered based on the LocationType. This allows multiple, comma delimited.</param>
/// <param name="isMissing">Optional filter by items that are missing episodes or not.</param> /// <param name="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="isUnaired">Optional filter by items that are unaired episodes or not.</param>
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param> /// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
@ -56,41 +56,41 @@ namespace Jellyfin.Api.Controllers
/// <param name="hasImdbId">Optional filter by items that have an imdb id 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="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="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
/// <param name="excludeItemIds">Optional. If specified, results will be filtered by exxcluding item ids. This allows multiple, comma delimeted.</param> /// <param name="excludeItemIds">Optional. If specified, results will be filtered by excluding item ids. This allows multiple, comma delimited.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param> /// <param name="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="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="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="searchTerm">Optional. Filter based on a search term.</param>
/// <param name="sortOrder">Sort Order - Ascending,Descending.</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="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimeted. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param> /// <param name="filters">Optional. Specify additional filters to apply. This allows multiple, comma delimited. Options: IsFolder, IsNotFolder, IsUnplayed, IsPlayed, IsFavorite, IsResumable, Likes, Dislikes.</param>
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param> /// <param name="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="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="imageTypes">Optional. If specified, results will be filtered based on those containing image types. This allows multiple, comma delimited.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <param name="isPlayed">Optional filter by items that are played, or not.</param> /// <param name="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="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimeted.</param> /// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimeted.</param> /// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimeted.</param> /// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</param>
/// <param name="enableUserData">Optional, include user data.</param> /// <param name="enableUserData">Optional, include user data.</param>
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</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="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="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="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="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="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
/// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimeted.</param> /// <param name="artists">Optional. If specified, results will be filtered based on artists. This allows multiple, pipe delimited.</param>
/// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimeted.</param> /// <param name="excludeArtistIds">Optional. If specified, results will be filtered based on artist id. This allows multiple, pipe delimited.</param>
/// <param name="artistIds">Optional. If specified, results will be filtered to include only those containing the specified artist id.</param> /// <param name="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="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="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="albums">Optional. If specified, results will be filtered based on album. This allows multiple, pipe delimited.</param>
/// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimeted.</param> /// <param name="albumIds">Optional. If specified, results will be filtered based on album id. This allows multiple, pipe delimited.</param>
/// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param> /// <param name="ids">Optional. If specific items are needed, specify a list of item id's to retrieve. This allows multiple, comma delimited.</param>
/// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimeted.</param> /// <param name="videoTypes">Optional filter by VideoType (videofile, dvd, bluray, iso). Allows multiple, comma delimited.</param>
/// <param name="minOfficialRating">Optional filter by minimum official rating (PG, PG-13, TV-MA, etc).</param> /// <param name="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="isLocked">Optional filter by items that are locked.</param>
/// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param> /// <param name="isPlaceHolder">Optional filter by items that are placeholders.</param>
@ -101,12 +101,12 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxWidth">Optional. Filter by the maximum width 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="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="is3D">Optional filter by items that are 3D, or not.</param>
/// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimeted.</param> /// <param name="seriesStatus">Optional filter by Series Status. Allows multiple, comma delimited.</param>
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param> /// <param name="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="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="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="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimeted.</param> /// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
/// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param> /// <param name="enableTotalRecordCount">Optional. Enable the total record count.</param>
/// <param name="enableImages">Optional, include image information in output.</param> /// <param name="enableImages">Optional, include image information in output.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the trailers.</returns>
@ -125,7 +125,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasParentalRating, [FromQuery] bool? hasParentalRating,
[FromQuery] bool? isHd, [FromQuery] bool? isHd,
[FromQuery] bool? is4K, [FromQuery] bool? is4K,
[FromQuery] string? locationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] locationTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] LocationType[] excludeLocationTypes,
[FromQuery] bool? isMissing, [FromQuery] bool? isMissing,
[FromQuery] bool? isUnaired, [FromQuery] bool? isUnaired,
@ -139,7 +139,7 @@ namespace Jellyfin.Api.Controllers
[FromQuery] bool? hasImdbId, [FromQuery] bool? hasImdbId,
[FromQuery] bool? hasTmdbId, [FromQuery] bool? hasTmdbId,
[FromQuery] bool? hasTvdbId, [FromQuery] bool? hasTvdbId,
[FromQuery] string? excludeItemIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeItemIds,
[FromQuery] int? startIndex, [FromQuery] int? startIndex,
[FromQuery] int? limit, [FromQuery] int? limit,
[FromQuery] bool? recursive, [FromQuery] bool? recursive,
@ -147,33 +147,33 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
[FromQuery] bool? isFavorite, [FromQuery] bool? isFavorite,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] string? genres, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
[FromQuery] string? officialRatings, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
[FromQuery] string? tags, [FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
[FromQuery] string? years, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
[FromQuery] string? person, [FromQuery] string? person,
[FromQuery] string? personIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
[FromQuery] string? personTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
[FromQuery] string? studios, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] studios,
[FromQuery] string? artists, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] artists,
[FromQuery] string? excludeArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] excludeArtistIds,
[FromQuery] string? artistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] artistIds,
[FromQuery] string? albumArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumArtistIds,
[FromQuery] string? contributingArtistIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] contributingArtistIds,
[FromQuery] string? albums, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] albums,
[FromQuery] string? albumIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] albumIds,
[FromQuery] string? ids, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids,
[FromQuery] string? videoTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] VideoType[] videoTypes,
[FromQuery] string? minOfficialRating, [FromQuery] string? minOfficialRating,
[FromQuery] bool? isLocked, [FromQuery] bool? isLocked,
[FromQuery] bool? isPlaceHolder, [FromQuery] bool? isPlaceHolder,
@ -184,16 +184,16 @@ namespace Jellyfin.Api.Controllers
[FromQuery] int? maxWidth, [FromQuery] int? maxWidth,
[FromQuery] int? maxHeight, [FromQuery] int? maxHeight,
[FromQuery] bool? is3D, [FromQuery] bool? is3D,
[FromQuery] string? seriesStatus, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SeriesStatus[] seriesStatus,
[FromQuery] string? nameStartsWithOrGreater, [FromQuery] string? nameStartsWithOrGreater,
[FromQuery] string? nameStartsWith, [FromQuery] string? nameStartsWith,
[FromQuery] string? nameLessThan, [FromQuery] string? nameLessThan,
[FromQuery] string? studioIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
[FromQuery] string? genreIds, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
[FromQuery] bool enableTotalRecordCount = true, [FromQuery] bool enableTotalRecordCount = true,
[FromQuery] bool? enableImages = true) [FromQuery] bool? enableImages = true)
{ {
var includeItemTypes = "Trailer"; var includeItemTypes = new[] { "Trailer" };
return _itemsController return _itemsController
.GetItems( .GetItems(

View File

@ -176,7 +176,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="seriesId">The series id.</param> /// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="season">Optional filter by season number.</param> /// <param name="season">Optional filter by season number.</param>
/// <param name="seasonId">Optional. Filter by season id.</param> /// <param name="seasonId">Optional. Filter by season id.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
@ -188,7 +188,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="imageTypeLimit">Optional, the max number of images to return, per image type.</param> /// <param name="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="enableImageTypes">Optional. The image types to include in the output.</param>
/// <param name="enableUserData">Optional. Include user data.</param> /// <param name="enableUserData">Optional. Include user data.</param>
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimeted. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param> /// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns> /// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the episodes on success or a <see cref="NotFoundResult"/> if the series was not found.</returns>
[HttpGet("{seriesId}/Episodes")] [HttpGet("{seriesId}/Episodes")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
@ -303,7 +303,7 @@ namespace Jellyfin.Api.Controllers
/// </summary> /// </summary>
/// <param name="seriesId">The series id.</param> /// <param name="seriesId">The series id.</param>
/// <param name="userId">The user id.</param> /// <param name="userId">The user id.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimeted. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines, TrailerUrls.</param>
/// <param name="isSpecialSeason">Optional. Filter by special season.</param> /// <param name="isSpecialSeason">Optional. Filter by special season.</param>
/// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param> /// <param name="isMissing">Optional. Filter by items that are missing episodes or not.</param>
/// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param> /// <param name="adjacentTo">Optional. Return items that are siblings of a supplied item.</param>

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
@ -96,7 +97,7 @@ namespace Jellyfin.Api.Controllers
[ProducesAudioFile] [ProducesAudioFile]
public async Task<ActionResult> GetUniversalAudioStream( public async Task<ActionResult> GetUniversalAudioStream(
[FromRoute, Required] Guid itemId, [FromRoute, Required] Guid itemId,
[FromQuery] string? container, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] container,
[FromQuery] string? mediaSourceId, [FromQuery] string? mediaSourceId,
[FromQuery] string? deviceId, [FromQuery] string? deviceId,
[FromQuery] Guid? userId, [FromQuery] Guid? userId,
@ -191,8 +192,11 @@ namespace Jellyfin.Api.Controllers
if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase)) if (!isStatic && string.Equals(mediaSource.TranscodingSubProtocol, "hls", StringComparison.OrdinalIgnoreCase))
{ {
// hls segment container can only be mpegts or fmp4 per ffmpeg documentation // 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 // 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 var dynamicHlsRequestDto = new HlsAudioRequestDto
{ {
@ -201,7 +205,7 @@ namespace Jellyfin.Api.Controllers
Static = isStatic, Static = isStatic,
PlaySessionId = info.PlaySessionId, PlaySessionId = info.PlaySessionId,
// fallback to mpegts if device reports some weird value unsupported by hls // 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, MediaSourceId = mediaSourceId,
DeviceId = deviceId, DeviceId = deviceId,
AudioCodec = audioCodec, AudioCodec = audioCodec,
@ -258,7 +262,7 @@ namespace Jellyfin.Api.Controllers
} }
private DeviceProfile GetDeviceProfile( private DeviceProfile GetDeviceProfile(
string? container, string[] containers,
string? transcodingContainer, string? transcodingContainer,
string? audioCodec, string? audioCodec,
string? transcodingProtocol, string? transcodingProtocol,
@ -270,7 +274,6 @@ namespace Jellyfin.Api.Controllers
{ {
var deviceProfile = new DeviceProfile(); var deviceProfile = new DeviceProfile();
var containers = RequestHelpers.Split(container, ',', true);
int len = containers.Length; int len = containers.Length;
var directPlayProfiles = new DirectPlayProfile[len]; var directPlayProfiles = new DirectPlayProfile[len];
for (int i = 0; i < len; i++) for (int i = 0; i < len; i++)
@ -327,7 +330,7 @@ namespace Jellyfin.Api.Controllers
if (conditions.Count > 0) if (conditions.Count > 0)
{ {
// codec profile // 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(); deviceProfile.CodecProfiles = codecProfiles.ToArray();

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">User id.</param> /// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param> /// <param name="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.</param> /// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimeted.</param> /// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="isPlayed">Filter by items that are played, or not.</param> /// <param name="isPlayed">Filter by items that are played, or not.</param>
/// <param name="enableImages">Optional. include image information in output.</param> /// <param name="enableImages">Optional. include image information in output.</param>
/// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param> /// <param name="imageTypeLimit">Optional. the max number of images to return, per image type.</param>
@ -269,7 +269,7 @@ namespace Jellyfin.Api.Controllers
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] Guid? parentId, [FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isPlayed, [FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages, [FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery new LatestItemsQuery
{ {
GroupItems = groupItems, GroupItems = groupItems,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true), IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed, IsPlayed = isPlayed,
Limit = limit, Limit = limit,
ParentId = parentId ?? Guid.Empty, ParentId = parentId ?? Guid.Empty,

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.Linq; using System.Linq;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.UserViewDtos; using Jellyfin.Api.Models.UserViewDtos;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -67,7 +68,7 @@ namespace Jellyfin.Api.Controllers
public ActionResult<QueryResult<BaseItemDto>> GetUserViews( public ActionResult<QueryResult<BaseItemDto>> GetUserViews(
[FromRoute, Required] Guid userId, [FromRoute, Required] Guid userId,
[FromQuery] bool? includeExternalContent, [FromQuery] bool? includeExternalContent,
[FromQuery] string? presetViews, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] presetViews,
[FromQuery] bool includeHidden = false) [FromQuery] bool includeHidden = false)
{ {
var query = new UserViewQuery var query = new UserViewQuery
@ -81,9 +82,9 @@ namespace Jellyfin.Api.Controllers
query.IncludeExternalContent = includeExternalContent.Value; 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; var app = _authContext.GetAuthorizationInfo(Request).Client ?? string.Empty;

View File

@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>
@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false); .ConfigureAwait(false);
TranscodingJobDto? job = null; 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); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try try
{ {
if (!System.IO.File.Exists(playlist)) if (!System.IO.File.Exists(playlistPath))
{ {
// If the playlist doesn't already exist, startup ffmpeg // If the playlist doesn't already exist, startup ffmpeg
try try
{ {
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodingJobHelper.StartFfMpeg(
state, state,
playlist, playlistPath,
GetCommandLineArguments(playlist, state), GetCommandLineArguments(playlistPath, state),
Request, Request,
TranscodingJobType, TranscodingJobType,
cancellationTokenSource) cancellationTokenSource)
@ -328,7 +329,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments; minSegments = state.MinSegments;
if (minSegments > 0) 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) if (job != null)
{ {
_transcodingJobHelper.OnTranscodeEndRequest(job); _transcodingJobHelper.OnTranscodeEndRequest(job);
} }
var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength); var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8")); return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
} }
@ -359,17 +360,46 @@ namespace Jellyfin.Api.Controllers
private string GetCommandLineArguments(string outputPath, StreamState state) private string GetCommandLineArguments(string outputPath, StreamState state)
{ {
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions); var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts"; 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 outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
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)) if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{ {
segmentFormat = "mpegts"; 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( var baseUrlParam = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -378,20 +408,19 @@ namespace Jellyfin.Api.Controllers
return string.Format( return string.Format(
CultureInfo.InvariantCulture, 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, inputModifier,
_encodingHelper.GetInputArgument(state, _encodingOptions), _encodingHelper.GetInputArgument(state, _encodingOptions),
threads, threads,
_encodingHelper.GetMapArgs(state), mapArgs,
GetVideoArguments(state), GetVideoArguments(state),
GetAudioArguments(state), GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture), state.SegmentLength.ToString(CultureInfo.InvariantCulture),
string.Empty,
segmentFormat, segmentFormat,
baseUrlParam, baseUrlParam,
outputPath, outputTsArg,
outputTsArg) outputPath).Trim();
.Trim();
} }
/// <summary> /// <summary>
@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for audio transcoding.</returns> /// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state) private string GetAudioArguments(StreamState state)
{ {
var codec = _encodingHelper.GetAudioEncoder(state); if (state.AudioStream == null)
if (EncodingHelper.IsCopyCodec(codec))
{ {
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; var channels = state.OutputAudioChannels;
@ -429,7 +497,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true); args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args; return args;
} }
@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for video transcoding.</returns> /// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state) private string GetVideoArguments(StreamState state)
{ {
if (state.VideoStream == null)
{
return string.Empty;
}
if (!state.IsOutputVideo) if (!state.IsOutputVideo)
{ {
return string.Empty; return string.Empty;
@ -450,46 +523,64 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec; 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) // if (state.EnableMpegtsM2TsMode)
// { // {
// args += " -mpegts_m2ts_mode 1"; // 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 (codec.Equals("copy", StringComparison.OrdinalIgnoreCase)) if (EncodingHelper.IsCopyCodec(codec))
{ {
// if h264_mp4toannexb is ever added, do not use it for live tv // If h264_mp4toannexb is ever added, do not use it for live tv.
if (state.VideoStream != null && if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{ {
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream); string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs)) if (!string.IsNullOrEmpty(bitStreamArgs))
{ {
args += " " + bitStreamArgs; args += " " + bitStreamArgs;
} }
} }
args += " -start_at_zero";
} }
else else
{ {
var keyFrameArg = string.Format( args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
CultureInfo.InvariantCulture,
" -force_key_frames \"expr:gte(t,n_forced*{0})\"", // Set the key frame params for video encoding to match the hls segment time.
state.SegmentLength.ToString(CultureInfo.InvariantCulture)); 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; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg; if (hasGraphicalSubs)
// Add resolution params, if specified
if (!hasGraphicalSubs)
{ {
// Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
else
{
// Resolution params.
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec); args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
} }
// This is for internal graphical subs if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
if (hasGraphicalSubs)
{ {
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec); args += " -start_at_zero";
} }
} }

View File

@ -10,6 +10,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -203,9 +204,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.RequiresElevation)] [Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [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)) .Select(i => _libraryManager.GetItemById(i))
.OfType<Video>() .OfType<Video>()
.OrderBy(i => i.Id) .OrderBy(i => i.Id)
@ -283,7 +284,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param> /// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param> /// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param> /// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param> /// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -312,7 +313,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param> /// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param> /// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param> /// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param> /// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param> /// <param name="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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param> /// <param name="liveStreamId">The live stream id.</param>

View File

@ -73,9 +73,9 @@ namespace Jellyfin.Api.Controllers
[FromQuery] string? sortOrder, [FromQuery] string? sortOrder,
[FromQuery] string? parentId, [FromQuery] string? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? excludeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
[FromQuery] string? includeItemTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] string? mediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
[FromQuery] string? sortBy, [FromQuery] string? sortBy,
[FromQuery] bool? enableUserData, [FromQuery] bool? enableUserData,
[FromQuery] int? imageTypeLimit, [FromQuery] int? imageTypeLimit,
@ -103,19 +103,15 @@ namespace Jellyfin.Api.Controllers
IList<BaseItem> items; 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) var query = new InternalItemsQuery(user)
{ {
ExcludeItemTypes = excludeItemTypesArr, ExcludeItemTypes = excludeItemTypes,
IncludeItemTypes = includeItemTypesArr, IncludeItemTypes = includeItemTypes,
MediaTypes = mediaTypesArr, MediaTypes = mediaTypes,
DtoOptions = dtoOptions DtoOptions = dtoOptions
}; };
bool Filter(BaseItem i) => FilterItem(i, excludeItemTypesArr, includeItemTypesArr, mediaTypesArr); bool Filter(BaseItem i) => FilterItem(i, excludeItemTypes, includeItemTypes, mediaTypes);
if (parentItem.IsFolder) if (parentItem.IsFolder)
{ {

View File

@ -207,7 +207,61 @@ namespace Jellyfin.Api.Helpers
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User); 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())) if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{ {
@ -217,40 +271,77 @@ namespace Jellyfin.Api.Helpers
var variation = GetBitrateVariation(totalBitrate); var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation; var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2; variation *= 2;
newBitrate = totalBitrate - variation; newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation); variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); 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(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=") .Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture)); .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)) if (!string.IsNullOrWhiteSpace(subtitleGroup))
{ {
builder.Append(",SUBTITLES=\"") playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup) .Append(subtitleGroup)
.Append('"'); .Append('"');
} }
builder.Append(Environment.NewLine); playlistBuilder.Append(Environment.NewLine);
builder.AppendLine(url); 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> /// <summary>
@ -419,15 +510,27 @@ namespace Jellyfin.Api.Helpers
/// <returns>H.26X level of the output video stream.</returns> /// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state) private int? GetOutputVideoCodecLevel(StreamState state)
{ {
string? levelString; string levelString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec) if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream != null
&& state.VideoStream.Level.HasValue) && state.VideoStream.Level.HasValue)
{ {
levelString = state.VideoStream?.Level.ToString(); levelString = state.VideoStream.Level.ToString() ?? string.Empty;
} }
else 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)) if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@ -438,6 +541,38 @@ namespace Jellyfin.Api.Helpers
return null; 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> /// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field. /// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary> /// </summary>
@ -468,6 +603,16 @@ namespace Jellyfin.Api.Helpers
return HlsCodecStringHelpers.GetEAC3String(); 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; return string.Empty;
} }
@ -492,15 +637,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase)) if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{ {
string? profile = state.GetRequestedProfiles("h264").FirstOrDefault(); string profile = GetOutputVideoCodecProfile(state, "h264");
return HlsCodecStringHelpers.GetH264String(profile, level); return HlsCodecStringHelpers.GetH264String(profile, level);
} }
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{ {
string? profile = state.GetRequestedProfiles("h265").FirstOrDefault(); string profile = GetOutputVideoCodecProfile(state, "hevc");
return HlsCodecStringHelpers.GetH265String(profile, level); return HlsCodecStringHelpers.GetH265String(profile, level);
} }
@ -544,12 +688,30 @@ namespace Jellyfin.Api.Helpers
return variation; return variation;
} }
private string ReplaceBitrate(string url, int oldValue, int newValue) private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{ {
return url.Replace( return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture), "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture), "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase); 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);
}
} }
} }

View File

@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers
/// </summary> /// </summary>
public static class HlsCodecStringHelpers 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> /// <summary>
/// Gets a MP3 codec string. /// Gets a MP3 codec string.
/// </summary> /// </summary>
/// <returns>MP3 codec string.</returns> /// <returns>MP3 codec string.</returns>
public static string GetMP3String() public static string GetMP3String()
{ {
return "mp4a.40.34"; return MP3;
} }
/// <summary> /// <summary>
@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers
return result.ToString(); 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> /// <summary>
/// Gets a H.264 codec string. /// Gets a H.264 codec string.
/// </summary> /// </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. // 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: // This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN] // 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 else
{ {
// Default to main if profile is invalid // Default to main if profile is invalid
result.Append(".1.6"); result.Append(".1.4");
} }
result.Append(".L") result.Append(".L")
.Append(level * 3) .Append(level)
.Append(".B0"); .Append(".B0");
return result.ToString(); 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";
}
} }
} }

View File

@ -1,8 +1,11 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; 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> /// <summary>
/// Gets the hls playlist text. /// Gets the hls playlist text.
/// </summary> /// </summary>
/// <param name="path">The path to the playlist file.</param> /// <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> /// <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 stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream); using var reader = new StreamReader(stream);
var text = reader.ReadToEnd(); 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); // Replace fMP4 init file URI.
text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.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);
return text; return text;
} }

View File

@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers
return session; 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( internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result, QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions, DtoOptions dtoOptions,

View File

@ -169,7 +169,9 @@ namespace Jellyfin.Api.Helpers
state.DirectStreamProvider = liveStreamInfo.Item2; 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); string? containerInternal = Path.GetExtension(state.RequestedUrl);
@ -187,7 +189,7 @@ namespace Jellyfin.Api.Helpers
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); 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; state.OutputAudioCodec = streamingRequest.AudioCodec;
@ -200,20 +202,41 @@ namespace Jellyfin.Api.Helpers
encodingHelper.TryStreamCopy(state); encodingHelper.TryStreamCopy(state);
if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{ {
var resolution = ResolutionNormalizer.Normalize( var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
state.VideoStream?.BitRate, && !state.VideoRequest.Height.HasValue
state.VideoStream?.Width, && !state.VideoRequest.MaxWidth.HasValue
state.VideoStream?.Height, && !state.VideoRequest.MaxHeight.HasValue;
state.OutputVideoBitrate.Value,
state.VideoStream?.Codec,
state.OutputVideoCodec,
state.VideoRequest.MaxWidth,
state.VideoRequest.MaxHeight);
state.VideoRequest.MaxWidth = resolution.MaxWidth; if (isVideoResolutionNotRequested
state.VideoRequest.MaxHeight = resolution.MaxHeight; && 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;
}
} }
} }

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
@ -145,7 +145,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs) lock (_activeTranscodingJobs)
{ {
// This is really only needed for HLS. // 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(); jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
} }
@ -241,7 +241,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs) lock (_activeTranscodingJobs)
{ {
// This is really only needed for HLS. // 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)); jobs.AddRange(_activeTranscodingJobs.Where(killJob));
} }
@ -304,10 +304,10 @@ namespace Jellyfin.Api.Helpers
process!.StandardInput.WriteLine("q"); process!.StandardInput.WriteLine("q");
// Need to wait because killing is asynchronous // Need to wait because killing is asynchronous.
if (!process.WaitForExit(5000)) if (!process.WaitForExit(5000))
{ {
_logger.LogInformation("Killing ffmpeg process for {Path}", job.Path); _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
process.Kill(); process.Kill();
} }
} }
@ -470,11 +470,11 @@ namespace Jellyfin.Api.Helpers
} }
/// <summary> /// <summary>
/// Starts the FFMPEG. /// Starts FFmpeg.
/// </summary> /// </summary>
/// <param name="state">The state.</param> /// <param name="state">The state.</param>
/// <param name="outputPath">The output path.</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="request">The <see cref="HttpRequest"/>.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param> /// <param name="cancellationTokenSource">The cancellation token source.</param>
@ -501,13 +501,13 @@ namespace Jellyfin.Api.Helpers
{ {
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); 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)) if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
{ {
throw new ArgumentException("FFMPEG path not set."); throw new ArgumentException("FFmpeg path not set.");
} }
var process = new Process var process = new Process
@ -544,18 +544,20 @@ namespace Jellyfin.Api.Helpers
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
_logger.LogInformation(commandLineLogMessage); _logger.LogInformation(commandLineLogMessage);
var logFilePrefix = "ffmpeg-transcode"; var logFilePrefix = "FFmpeg.Transcode-";
if (state.VideoRequest != null if (state.VideoRequest != null
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) && EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{ {
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec) logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
? "ffmpeg-remux" ? "FFmpeg.Remux-"
: "ffmpeg-directstream"; : "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); 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); 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,20 +571,20 @@ namespace Jellyfin.Api.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error starting ffmpeg"); _logger.LogError(ex, "Error starting FFmpeg");
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw; throw;
} }
_logger.LogDebug("Launched ffmpeg process"); _logger.LogDebug("Launched FFmpeg process");
state.TranscodingJob = transcodingJob; 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); _ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
// Wait for the file to exist before proceeeding // Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath; var ffmpegTargetFile = state.WaitForPath ?? outputPath;
_logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile); _logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited) while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
@ -748,11 +750,11 @@ namespace Jellyfin.Api.Helpers
if (process.ExitCode == 0) if (process.ExitCode == 0)
{ {
_logger.LogInformation("FFMpeg exited with code 0"); _logger.LogInformation("FFmpeg exited with code 0");
} }
else else
{ {
_logger.LogError("FFMpeg exited with code {0}", process.ExitCode); _logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
} }
process.Dispose(); process.Dispose();
@ -771,8 +773,9 @@ namespace Jellyfin.Api.Helpers
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken }, new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .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) if (state.VideoRequest != null)
{ {

View 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;
}
}
}

View File

@ -16,7 +16,8 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary> /// <summary>
/// Gets or sets the channels to return guide information for. /// Gets or sets the channels to return guide information for.
/// </summary> /// </summary>
public string? ChannelIds { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> ChannelIds { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets optional. Filter by user id. /// Gets or sets optional. Filter by user id.
@ -115,12 +116,14 @@ namespace Jellyfin.Api.Models.LiveTvDtos
/// <summary> /// <summary>
/// Gets or sets the genres to return guide information for. /// Gets or sets the genres to return guide information for.
/// </summary> /// </summary>
public string? Genres { get; set; } [JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
public IReadOnlyList<string> Genres { get; set; } = Array.Empty<string>();
/// <summary> /// <summary>
/// Gets or sets the genre ids to return guide information for. /// Gets or sets the genre ids to return guide information for.
/// </summary> /// </summary>
public string? GenreIds { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> GenreIds { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets include image information in output. /// Gets or sets include image information in output.

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using MediaBrowser.Common.Json.Converters;
namespace Jellyfin.Api.Models.PlaylistDtos namespace Jellyfin.Api.Models.PlaylistDtos
{ {
@ -15,7 +18,8 @@ namespace Jellyfin.Api.Models.PlaylistDtos
/// <summary> /// <summary>
/// Gets or sets item ids to add to the playlist. /// Gets or sets item ids to add to the playlist.
/// </summary> /// </summary>
public string? Ids { get; set; } [JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
public IReadOnlyList<Guid> Ids { get; set; } = Array.Empty<Guid>();
/// <summary> /// <summary>
/// Gets or sets the user id. /// Gets or sets the user id.

View File

@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the next item in the collection. /// Gets or sets the next item in the collection.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// TODO check if this properly updated dependant and has the proper principal relationship. /// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks> /// </remarks>
public virtual CollectionItem Next { get; set; } public virtual CollectionItem Next { get; set; }
@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the previous item in the collection. /// Gets or sets the previous item in the collection.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// TODO check if this properly updated dependant and has the proper principal relationship. /// TODO check if this properly updated Dependant and has the proper principal relationship.
/// </remarks> /// </remarks>
public virtual CollectionItem Previous { get; set; } public virtual CollectionItem Previous { get; set; }

View File

@ -141,7 +141,7 @@ namespace Jellyfin.Data.Entities.Libraries
public virtual ICollection<PersonRole> PersonRoles { get; protected set; } public virtual ICollection<PersonRole> PersonRoles { get; protected set; }
/// <summary> /// <summary>
/// Gets or sets a collection containing the generes for this item. /// Gets or sets a collection containing the genres for this item.
/// </summary> /// </summary>
public virtual ICollection<Genre> Genres { get; protected set; } public virtual ICollection<Genre> Genres { get; protected set; }

View File

@ -53,7 +53,7 @@ namespace Jellyfin.Server.Implementations.Users
bool success = false; bool success = false;
// As long as jellyfin supports passwordless users, we need this little block here to accommodate // As long as jellyfin supports password-less users, we need this little block here to accommodate
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password)) if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
{ {
return Task.FromResult(new ProviderAuthenticationResult return Task.FromResult(new ProviderAuthenticationResult

View File

@ -43,7 +43,8 @@ namespace MediaBrowser.Common.Json.Converters
} }
catch (FormatException) 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."); // _logger.LogWarning(e, "Error converting value.");
} }
} }

View File

@ -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));
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -42,6 +42,7 @@ namespace MediaBrowser.Common.Json
options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonGuidConverter());
options.Converters.Add(new JsonStringEnumConverter()); options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverterFactory()); options.Converters.Add(new JsonNullableStructConverterFactory());
options.Converters.Add(new JsonDateTimeIso8601Converter());
return options; return options;
} }

View File

@ -20,7 +20,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.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" /> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup> </ItemGroup>

View File

@ -247,7 +247,23 @@ namespace MediaBrowser.Common.Plugins
} }
catch 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> /// </summary>
public virtual void SaveConfiguration() public virtual void SaveConfiguration()
{ {
lock (_configurationSaveLock) SaveConfiguration(Configuration);
{
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -274,7 +285,7 @@ namespace MediaBrowser.Common.Plugins
Configuration = (TConfigurationType)configuration; Configuration = (TConfigurationType)configuration;
SaveConfiguration(); SaveConfiguration(Configuration);
ConfigurationChanged?.Invoke(this, configuration); ConfigurationChanged?.Invoke(this, configuration);
} }

View File

@ -2557,7 +2557,7 @@ namespace MediaBrowser.Controller.Entities
{ {
if (!AllowsMultipleImages(type)) if (!AllowsMultipleImages(type))
{ {
throw new ArgumentException("The change index operation is only applicable to backdrops and screenshots"); throw new ArgumentException("The change index operation is only applicable to backdrops and screen shots");
} }
var info1 = GetImageInfo(type, index1); var info1 = GetImageInfo(type, index1);

View File

@ -212,7 +212,7 @@ namespace MediaBrowser.Controller.Entities
/// <summary> /// <summary>
/// Loads our children. Validation will occur externally. /// Loads our children. Validation will occur externally.
/// We want this sychronous. /// We want this synchronous.
/// </summary> /// </summary>
protected virtual List<BaseItem> LoadChildren() protected virtual List<BaseItem> LoadChildren()
{ {
@ -1067,12 +1067,12 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
if (request.Genres.Length > 0) if (request.Genres.Count > 0)
{ {
return false; return false;
} }
if (request.GenreIds.Length > 0) if (request.GenreIds.Count > 0)
{ {
return false; return false;
} }
@ -1177,7 +1177,7 @@ namespace MediaBrowser.Controller.Entities
return false; return false;
} }
if (request.GenreIds.Length > 0) if (request.GenreIds.Count > 0)
{ {
return false; return false;
} }

View File

@ -46,7 +46,7 @@ namespace MediaBrowser.Controller.Entities
public string[] ExcludeInheritedTags { get; set; } public string[] ExcludeInheritedTags { get; set; }
public string[] Genres { get; set; } public IReadOnlyList<string> Genres { get; set; }
public bool? IsSpecialSeason { get; set; } public bool? IsSpecialSeason { get; set; }
@ -116,7 +116,7 @@ namespace MediaBrowser.Controller.Entities
public Guid[] StudioIds { get; set; } public Guid[] StudioIds { get; set; }
public Guid[] GenreIds { get; set; } public IReadOnlyList<Guid> GenreIds { get; set; }
public ImageType[] ImageTypes { get; set; } public ImageType[] ImageTypes { get; set; }
@ -162,7 +162,7 @@ namespace MediaBrowser.Controller.Entities
public double? MinCommunityRating { get; set; } public double? MinCommunityRating { get; set; }
public Guid[] ChannelIds { get; set; } public IReadOnlyList<Guid> ChannelIds { get; set; }
public int? ParentIndexNumber { get; set; } public int? ParentIndexNumber { get; set; }

View File

@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
} }
// Apply genre filter // 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; return false;
} }
@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities
} }
// Apply genre filter // 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); var genreItem = libraryManager.GetItemById(id);
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase); return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
public class EncodingHelper 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 IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
// Only use alternative encoders for video files. // Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (state.VideoType == VideoType.VideoFile) if (state.VideoType == VideoType.VideoFile)
{ {
var hwType = encodingOptions.HardwareAccelerationType; var hwType = encodingOptions.HardwareAccelerationType;
@ -247,7 +248,7 @@ namespace MediaBrowser.Controller.MediaEncoding
return null; return null;
} }
// Seeing reported failures here, not sure yet if this is related to specfying input format // Seeing reported failures here, not sure yet if this is related to specifying input format
if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase)) if (string.Equals(container, "m4v", StringComparison.OrdinalIgnoreCase))
{ {
return null; return null;
@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libopus"; return "libopus";
} }
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
{
// flac is experimental in mp4 muxer
return "flac -strict -2";
}
return codec.ToLowerInvariant(); return codec.ToLowerInvariant();
} }
@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns> /// <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; var codec = stream.Codec ?? string.Empty;
@ -581,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1; || codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
} }
public bool IsH265(MediaStream stream) public static bool IsH265(MediaStream stream)
{ {
var codec = stream.Codec ?? string.Empty; var codec = stream.Codec ?? string.Empty;
@ -589,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1; || codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
} }
// TODO This is auto inserted into the mpegts mux so it might not be needed public static bool IsAAC(MediaStream stream)
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
public string GetBitStreamArgs(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)) if (IsH264(stream))
{ {
return "-bsf:v h264_mp4toannexb"; return "-bsf:v h264_mp4toannexb";
@ -601,12 +615,44 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
return "-bsf:v hevc_mp4toannexb"; return "-bsf:v hevc_mp4toannexb";
} }
else if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
else else
{ {
return null; 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) public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{ {
var bitrate = state.OutputVideoBitrate; var bitrate = state.OutputVideoBitrate;
@ -654,16 +700,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty; 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))
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)))
{ {
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; return level;
@ -766,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding
return null; 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> /// <summary>
/// Gets the video bitrate to specify on the command line. /// Gets the video bitrate to specify on the command line.
/// </summary> /// </summary>
@ -773,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var param = string.Empty; 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 && var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase); string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase); var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@ -781,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset)) if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{ {
param += "-preset " + encodingOptions.EncoderPreset; param += " -preset " + encodingOptions.EncoderPreset;
} }
else else
{ {
param += "-preset " + defaultPreset; param += " -preset " + defaultPreset;
} }
int encodeCrf = encodingOptions.H264Crf; int encodeCrf = encodingOptions.H264Crf;
@ -809,38 +976,40 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf; 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" }; string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase)) if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{ {
param += "-preset " + encodingOptions.EncoderPreset; param += " -preset " + encodingOptions.EncoderPreset;
} }
else else
{ {
param += "-preset 7"; param += " -preset 7";
} }
param += " -look_ahead 0"; param += " -look_ahead 0";
} }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc) 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) switch (encodingOptions.EncoderPreset)
{ {
case "veryslow": 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; break;
case "slow": case "slow":
case "slower": case "slower":
param += "-preset slow"; param += " -preset slow";
break; break;
case "medium": case "medium":
param += "-preset medium"; param += " -preset medium";
break; break;
case "fast": case "fast":
@ -848,27 +1017,27 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast": case "veryfast":
case "superfast": case "superfast":
case "ultrafast": case "ultrafast":
param += "-preset fast"; param += " -preset fast";
break; break;
default: default:
param += "-preset default"; param += " -preset default";
break; break;
} }
} }
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
{ {
switch (encodingOptions.EncoderPreset) switch (encodingOptions.EncoderPreset)
{ {
case "veryslow": case "veryslow":
case "slow": case "slow":
case "slower": case "slower":
param += "-quality quality"; param += " -quality quality";
break; break;
case "medium": case "medium":
param += "-quality balanced"; param += " -quality balanced";
break; break;
case "fast": case "fast":
@ -876,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast": case "veryfast":
case "superfast": case "superfast":
case "ultrafast": case "ultrafast":
param += "-quality speed"; param += " -quality speed";
break; break;
default: default:
param += "-quality speed"; param += " -quality speed";
break; break;
} }
@ -896,6 +1065,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// Enhance workload when tone mapping with AMF on some APUs // Enhance workload when tone mapping with AMF on some APUs
param += " -preanalysis true"; 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 else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{ {
@ -917,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2); profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/ // 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), profileScore.ToString(_usCulture),
crf, crf,
qmin, qmin,
@ -925,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase)) 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 else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
{ {
param += "-qmin 2"; param += " -qmin 2";
} }
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{ {
param += "-mbd 2"; param += " -mbd 2";
} }
param += GetVideoBitrateParam(state, videoEncoder); param += GetVideoBitrateParam(state, videoEncoder);
@ -945,11 +1119,25 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
var targetVideoCodec = state.ActualOutputVideoCodec; 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(); 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, // Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
// which is compatible (and ugly) 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) if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& profile != null && profile != null
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1) && profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
@ -957,13 +1145,31 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_baseline"; 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.IsNullOrEmpty(profile))
{ {
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)) && !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{ {
// not supported by h264_omx // 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)) 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 // libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "libx265", 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 " + hevcLevel / 3;
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;
} }
} }
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) || string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{ {
// nvenc doesn't decode with param -level set ?! param += " -level " + level;
// TODO:
} }
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; param += " -level " + level;
} }
@ -1032,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase)) if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{ {
// todo // 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.
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase) // TODO: set fine tuned params.
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) param += " -x265-params:0 no-info=1";
&& !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;
} }
return param; return param;
@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{ {
return .5; return .6;
} }
return 1; return 1;
@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream) public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{ {
if (audioStream == null) return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
{
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;
} }
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream) public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
{ {
if (audioStream == null) if (audioStream == null)
{ {
return 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); 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 // Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested // Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options // https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0) if (filters.Count > 0)
{ {
return "-af \"" + string.Join(",", filters) + "\""; return " -af \"" + string.Join(",", filters) + "\"";
} }
return string.Empty; return string.Empty;
@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{System.Int32}.</returns> /// <returns>System.Nullable{System.Int32}.</returns>
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec) public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
{ {
if (audioStream == null)
{
return null;
}
var request = state.BaseRequest; var request = state.BaseRequest;
var inputChannels = audioStream?.Channels; var inputChannels = audioStream?.Channels;
@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// libmp3lame currently only supports two channel output // libmp3lame currently only supports two channel output
transcoderChannelLimit = 2; transcoderChannelLimit = 2;
} }
else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
{
// aac is able to handle 8ch(7.1 layout)
transcoderChannelLimit = 8;
}
else 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 // 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 // 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"; videoSizeParam += ",hwupload=extra_hw_frames=64";
} }
@ -1729,7 +1907,8 @@ namespace MediaBrowser.Controller.MediaEncoding
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable // 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 [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 // 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 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 [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\""; 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. QSV in FFMpeg can now setup hardware overlay for transcodes.
@ -1776,7 +1957,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam); videoSizeParam);
} }
private (int? width, int? height) GetFixedOutputSize( public static (int? width, int? height) GetFixedOutputSize(
int? videoWidth, int? videoWidth,
int? videoHeight, int? videoHeight,
int? requestedWidth, int? requestedWidth,
@ -1836,7 +2017,9 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxHeight); requestedMaxHeight);
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) 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 && width.HasValue
&& height.HasValue) && height.HasValue)
{ {
@ -1845,7 +2028,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even. // output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value; var outputWidth = width.Value;
var outputHeight = height.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) var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true) || state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true) || state.DeInterlace("h265", true)
@ -2107,10 +2291,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1; var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1; var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_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 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 isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1; var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", 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 isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isColorDepth10 = IsColorDepth10(state); var isColorDepth10 = IsColorDepth10(state);
@ -2185,6 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwdownload"); filters.Add("hwdownload");
if (isLibX264Encoder if (isLibX264Encoder
|| isLibX265Encoder
|| hasGraphicalSubs || hasGraphicalSubs
|| (isNvdecHevcDecoder && isDeinterlaceHevc) || (isNvdecHevcDecoder && isDeinterlaceHevc)
|| (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc)) || (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@ -2195,20 +2383,20 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// When the input may or may not be hardware VAAPI decodable // When the input may or may not be hardware VAAPI decodable
if (isVaapiH264Encoder) if (isVaapiH264Encoder || isVaapiHevcEncoder)
{ {
filters.Add("format=nv12|vaapi"); filters.Add("format=nv12|vaapi");
filters.Add("hwupload"); filters.Add("hwupload");
} }
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context // 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"); filters.Add("hwupload=extra_hw_frames=64");
} }
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first // 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(); var codec = videoStream.Codec.ToLowerInvariant();
@ -2250,7 +2438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Add software deinterlace filter before scaling filter // Add software deinterlace filter before scaling filter
if ((isDeinterlaceH264 || isDeinterlaceHevc) if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder && !isVaapiH264Encoder
&& !isVaapiHevcEncoder
&& !isQsvH264Encoder && !isQsvH264Encoder
&& !isQsvHevcEncoder
&& !isNvdecH264Decoder) && !isNvdecH264Decoder)
{ {
if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) 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) // Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (isVaapiH264Encoder) if (isVaapiH264Encoder || isVaapiHevcEncoder)
{ {
if (hasTextSubs) if (hasTextSubs)
{ {
@ -2329,7 +2519,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary> /// <summary>
/// Gets the number of threads. /// Gets the number of threads.
/// </summary> /// </summary>
public int GetNumberOfThreads(EncodingJobInfo state, EncodingOptions encodingOptions, string outputVideoCodec) #nullable enable
public static int GetNumberOfThreads(EncodingJobInfo? state, EncodingOptions encodingOptions, string? outputVideoCodec)
{ {
if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase)) if (string.Equals(outputVideoCodec, "libvpx", StringComparison.OrdinalIgnoreCase))
{ {
@ -2339,17 +2530,21 @@ namespace MediaBrowser.Controller.MediaEncoding
return Math.Max(Environment.ProcessorCount - 1, 1); return Math.Max(Environment.ProcessorCount - 1, 1);
} }
var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount; var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
// Automatic // Automatic
if (threads <= 0 || threads >= Environment.ProcessorCount) if (threads <= 0)
{ {
return 0; return 0;
}
else if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
} }
return threads; return threads;
} }
#nullable disable
public void TryStreamCopy(EncodingJobInfo state) public void TryStreamCopy(EncodingJobInfo state)
{ {
if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream)) if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
@ -2557,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public void AttachMediaSourceInfo( public void AttachMediaSourceInfo(
EncodingJobInfo state, EncodingJobInfo state,
EncodingOptions encodingOptions,
MediaSourceInfo mediaSource, MediaSourceInfo mediaSource,
string requestedUrl) string requestedUrl)
{ {
@ -2687,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i)) request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault(); ?? 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) 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) if (audioCodecs.Count < 2)
{ {
return; return;
@ -2719,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) private void NormalizeSubtitleEmbed(EncodingJobInfo state)
{ {
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed) if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@ -2752,7 +2988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile; var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// Only use alternative encoders for video files. // Only use alternative encoders for video files.
// When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully // When using concat with folder rips, if the mfx session fails to initialize, ffmpeg will be stuck retrying and will not exit gracefully
// Since transcoding of folder rips is expiremental anyway, it's not worth adding additional variables such as this. // Since transcoding of folder rips is experimental anyway, it's not worth adding additional variables such as this.
if (videoType != VideoType.VideoFile) if (videoType != VideoType.VideoFile)
{ {
return null; return null;
@ -3352,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture); args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
} }
args += " " + GetAudioFilterParam(state, encodingOptions, false); args += GetAudioFilterParam(state, encodingOptions, false);
return args; return args;
} }

View File

@ -593,6 +593,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
get get
{ {
if (VideoStream == null)
{
return null;
}
if (EncodingHelper.IsCopyCodec(OutputVideoCodec)) if (EncodingHelper.IsCopyCodec(OutputVideoCodec))
{ {
return VideoStream?.Codec; return VideoStream?.Codec;
@ -606,6 +611,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
get get
{ {
if (AudioStream == null)
{
return null;
}
if (EncodingHelper.IsCopyCodec(OutputAudioCodec)) if (EncodingHelper.IsCopyCodec(OutputAudioCodec))
{ {
return AudioStream?.Codec; return AudioStream?.Codec;

View File

@ -31,7 +31,7 @@ namespace MediaBrowser.Controller.Playlists
/// <param name="itemIds">The item ids.</param> /// <param name="itemIds">The item ids.</param>
/// <param name="userId">The user identifier.</param> /// <param name="userId">The user identifier.</param>
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId); Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId);
/// <summary> /// <summary>
/// Removes from playlist. /// Removes from playlist.

View File

@ -64,6 +64,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string _ffmpegPath = string.Empty; private string _ffmpegPath = string.Empty;
private string _ffprobePath; private string _ffprobePath;
private int threads;
public MediaEncoder( public MediaEncoder(
ILogger<MediaEncoder> logger, ILogger<MediaEncoder> logger,
@ -129,6 +130,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableDecoders(validator.GetDecoders()); SetAvailableDecoders(validator.GetDecoders());
SetAvailableEncoders(validator.GetEncoders()); SetAvailableEncoders(validator.GetEncoders());
SetAvailableHwaccels(validator.GetHwaccels()); SetAvailableHwaccels(validator.GetHwaccels());
threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
} }
_logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty); _logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
@ -377,9 +379,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var args = extractChapters var args = extractChapters
? "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_chapters -show_format" ? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads 0 -v warning -print_format json -show_streams -show_format"; : "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath).Trim(); args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
var process = new Process var process = new Process
{ {
@ -520,29 +522,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg"); var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
// apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar then scale to width 600. // apply some filters to thumbnail extracted below (below) crop any black lines that we made and get the correct ar.
// This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar // This filter chain may have adverse effects on recorded tv thumbnails if ar changes during presentation ex. commercials @ diff ar
var vf = "scale=600:trunc(600/dar/2)*2"; var vf = string.Empty;
if (threedFormat.HasValue) if (threedFormat.HasValue)
{ {
switch (threedFormat.Value) switch (threedFormat.Value)
{ {
case Video3DFormat.HalfSideBySide: case Video3DFormat.HalfSideBySide:
vf = "crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2"; vf = "-vf crop=iw/2:ih:0:0,scale=(iw*2):ih,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
// hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not. // hsbs crop width in half,scale to correct size, set the display aspect,crop out any black bars we may have made. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
break; break;
case Video3DFormat.FullSideBySide: case Video3DFormat.FullSideBySide:
vf = "crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2"; vf = "-vf crop=iw/2:ih:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to 600. // fsbs crop width in half,set the display aspect,crop out any black bars we may have made
break; break;
case Video3DFormat.HalfTopAndBottom: case Video3DFormat.HalfTopAndBottom:
vf = "crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2"; vf = "-vf crop=iw:ih/2:0:0,scale=(iw*2):ih),setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
// htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made the scale width to 600 // htab crop heigh in half,scale to correct size, set the display aspect,crop out any black bars we may have made
break; break;
case Video3DFormat.FullTopAndBottom: case Video3DFormat.FullTopAndBottom:
vf = "crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1,scale=600:trunc(600/dar/2)*2"; vf = "-vf crop=iw:ih/2:0:0,setdar=dar=a,crop=min(iw\\,ih*dar):min(ih\\,iw/dar):(iw-min(iw\\,iw*sar))/2:(ih - min (ih\\,ih/sar))/2,setsar=sar=1";
// ftab crop heigt in half, set the display aspect,crop out any black bars we may have made the scale width to 600 // ftab crop heigt in half, set the display aspect,crop out any black bars we may have made
break; break;
default: default:
break; break;
@ -555,8 +557,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
// Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case. // Use ffmpeg to sample 100 (we can drop this if required using thumbnail=50 for 50 frames) frames and pick the best thumbnail. Have a fall back just in case.
var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty; var thumbnail = enableThumbnail ? ",thumbnail=24" : string.Empty;
var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}{4}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail) : var args = useIFrame ? string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {5} -v quiet -vframes 1 {2}{4} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, thumbnail, threads) :
string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg); string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
@ -693,7 +695,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
Directory.CreateDirectory(targetDirectory); Directory.CreateDirectory(targetDirectory);
var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg"); var outputPath = Path.Combine(targetDirectory, filenamePrefix + "%05d.jpg");
var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads 0 -v quiet -vf \"{2}\" -f image2 \"{1}\"", inputArgument, outputPath, vf); var args = string.Format(CultureInfo.InvariantCulture, "-i {0} -threads {3} -v quiet {2} -f image2 \"{1}\"", inputArgument, outputPath, vf, threads);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1); var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1); var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);

View File

@ -234,8 +234,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var channelsValue = channels.Value; var channelsValue = channels.Value;
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) || if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
{ {
if (channelsValue <= 2) if (channelsValue <= 2)
{ {
@ -248,6 +248,34 @@ namespace MediaBrowser.MediaEncoding.Probing
} }
} }
if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
{
if (channelsValue <= 2)
{
return 192000;
}
if (channelsValue >= 5)
{
return 640000;
}
}
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "alac", StringComparison.OrdinalIgnoreCase))
{
if (channelsValue <= 2)
{
return 960000;
}
if (channelsValue >= 5)
{
return 2880000;
}
}
return null; return null;
} }
@ -774,6 +802,35 @@ namespace MediaBrowser.MediaEncoding.Probing
stream.BitRate = bitrate; stream.BitRate = bitrate;
} }
// Extract bitrate info from tag "BPS" if possible.
if (!stream.BitRate.HasValue
&& (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
{
var bps = GetBPSFromTags(streamInfo);
if (bps != null && bps > 0)
{
stream.BitRate = bps;
}
}
// Get average bitrate info from tag "NUMBER_OF_BYTES" and "DURATION" if possible.
if (!stream.BitRate.HasValue
&& (string.Equals(streamInfo.CodecType, "audio", StringComparison.OrdinalIgnoreCase)
|| string.Equals(streamInfo.CodecType, "video", StringComparison.OrdinalIgnoreCase)))
{
var durationInSeconds = GetRuntimeSecondsFromTags(streamInfo);
var bytes = GetNumberOfBytesFromTags(streamInfo);
if (durationInSeconds != null && bytes != null)
{
var bps = Convert.ToInt32(bytes * 8 / durationInSeconds, CultureInfo.InvariantCulture);
if (bps > 0)
{
stream.BitRate = bps;
}
}
}
var disposition = streamInfo.Disposition; var disposition = streamInfo.Disposition;
if (disposition != null) if (disposition != null)
{ {
@ -963,6 +1020,50 @@ namespace MediaBrowser.MediaEncoding.Probing
} }
} }
private int? GetBPSFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo != null && streamInfo.Tags != null)
{
var bps = GetDictionaryValue(streamInfo.Tags, "BPS-eng") ?? GetDictionaryValue(streamInfo.Tags, "BPS");
if (!string.IsNullOrEmpty(bps)
&& int.TryParse(bps, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBps))
{
return parsedBps;
}
}
return null;
}
private double? GetRuntimeSecondsFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo != null && streamInfo.Tags != null)
{
var duration = GetDictionaryValue(streamInfo.Tags, "DURATION-eng") ?? GetDictionaryValue(streamInfo.Tags, "DURATION");
if (!string.IsNullOrEmpty(duration) && TimeSpan.TryParse(duration, out var parsedDuration))
{
return parsedDuration.TotalSeconds;
}
}
return null;
}
private long? GetNumberOfBytesFromTags(MediaStreamInfo streamInfo)
{
if (streamInfo != null && streamInfo.Tags != null)
{
var numberOfBytes = GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES-eng") ?? GetDictionaryValue(streamInfo.Tags, "NUMBER_OF_BYTES");
if (!string.IsNullOrEmpty(numberOfBytes)
&& long.TryParse(numberOfBytes, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedBytes))
{
return parsedBytes;
}
}
return null;
}
private void SetSize(InternalMediaInfoResult data, MediaInfo info) private void SetSize(InternalMediaInfoResult data, MediaInfo info)
{ {
if (data.Format != null) if (data.Format != null)

View File

@ -67,6 +67,8 @@ namespace MediaBrowser.Model.Configuration
public bool EnableHardwareEncoding { get; set; } public bool EnableHardwareEncoding { get; set; }
public bool AllowHevcEncoding { get; set; }
public bool EnableSubtitleExtraction { get; set; } public bool EnableSubtitleExtraction { get; set; }
public string[] HardwareDecodingCodecs { get; set; } public string[] HardwareDecodingCodecs { get; set; }
@ -99,6 +101,7 @@ namespace MediaBrowser.Model.Configuration
EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Hevc = true;
EnableDecodingColorDepth10Vp9 = true; EnableDecodingColorDepth10Vp9 = true;
EnableHardwareEncoding = true; EnableHardwareEncoding = true;
AllowHevcEncoding = true;
EnableSubtitleExtraction = true; EnableSubtitleExtraction = true;
HardwareDecodingCodecs = new string[] { "h264", "vc1" }; HardwareDecodingCodecs = new string[] { "h264", "vc1" };
} }

View File

@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Dlna
new ResolutionConfiguration(720, 950000), new ResolutionConfiguration(720, 950000),
new ResolutionConfiguration(1280, 2500000), new ResolutionConfiguration(1280, 2500000),
new ResolutionConfiguration(1920, 4000000), new ResolutionConfiguration(1920, 4000000),
new ResolutionConfiguration(2560, 8000000), new ResolutionConfiguration(2560, 20000000),
new ResolutionConfiguration(3840, 35000000) new ResolutionConfiguration(3840, 35000000)
}; };
@ -29,7 +29,7 @@ namespace MediaBrowser.Model.Dlna
int? maxWidth, int? maxWidth,
int? maxHeight) int? maxHeight)
{ {
// If the bitrate isn't changing, then don't downlscale the resolution // If the bitrate isn't changing, then don't downscale the resolution
if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value) if (inputBitrate.HasValue && outputBitrate >= inputBitrate.Value)
{ {
if (maxWidth.HasValue || maxHeight.HasValue) if (maxWidth.HasValue || maxHeight.HasValue)
@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
private static double GetVideoBitrateScaleFactor(string codec) private static double GetVideoBitrateScaleFactor(string codec)
{ {
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) || if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) || || string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase)) || string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{ {
return .5; return .6;
} }
return 1; return 1;

View File

@ -872,11 +872,34 @@ namespace MediaBrowser.Model.Dlna
return playlistItem; return playlistItem;
} }
private static int GetDefaultAudioBitrateIfUnknown(MediaStream audioStream) private static int GetDefaultAudioBitrate(string audioCodec, int? audioChannels)
{ {
if ((audioStream.Channels ?? 0) >= 6) if (!string.IsNullOrEmpty(audioCodec))
{ {
return 384000; // Default to a higher bitrate for stream copy
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 ((audioChannels ?? 0) < 2)
{
return 128000;
}
return (audioChannels ?? 0) >= 6 ? 640000 : 384000;
}
if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
{
if ((audioChannels ?? 0) < 2)
{
return 768000;
}
return (audioChannels ?? 0) >= 6 ? 3584000 : 1536000;
}
} }
return 192000; return 192000;
@ -897,14 +920,27 @@ namespace MediaBrowser.Model.Dlna
} }
else else
{ {
if (targetAudioChannels.HasValue && audioStream.Channels.HasValue && targetAudioChannels.Value < audioStream.Channels.Value) if (targetAudioChannels.HasValue
&& audioStream.Channels.HasValue
&& audioStream.Channels.Value > targetAudioChannels.Value)
{ {
// Reduce the bitrate if we're downmixing // Reduce the bitrate if we're downmixing.
defaultBitrate = targetAudioChannels.Value < 2 ? 128000 : 192000; defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
}
else if (targetAudioChannels.HasValue
&& audioStream.Channels.HasValue
&& audioStream.Channels.Value <= targetAudioChannels.Value
&& !string.IsNullOrEmpty(audioStream.Codec)
&& targetAudioCodecs != null
&& targetAudioCodecs.Length > 0
&& !Array.Exists(targetAudioCodecs, elem => string.Equals(audioStream.Codec, elem, StringComparison.OrdinalIgnoreCase)))
{
// Shift the bitrate if we're transcoding to a different audio codec.
defaultBitrate = GetDefaultAudioBitrate(targetAudioCodec, audioStream.Channels.Value);
} }
else else
{ {
defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream); defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
} }
// Seeing webm encoding failures when source has 1 audio channel and 22k bitrate. // Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
@ -938,8 +974,28 @@ namespace MediaBrowser.Model.Dlna
{ {
return 448000; return 448000;
} }
else if (totalBitrate <= 4000000)
{
return 640000;
}
else if (totalBitrate <= 5000000)
{
return 768000;
}
else if (totalBitrate <= 10000000)
{
return 1536000;
}
else if (totalBitrate <= 15000000)
{
return 2304000;
}
else if (totalBitrate <= 20000000)
{
return 3584000;
}
return 640000; return 7168000;
} }
private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile( private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile(

View File

@ -794,7 +794,7 @@ namespace MediaBrowser.Model.Dlna
public int? GetTargetAudioChannels(string codec) public int? GetTargetAudioChannels(string codec)
{ {
var defaultValue = GlobalMaxAudioChannels; var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
var value = GetOption(codec, "audiochannels"); var value = GetOption(codec, "audiochannels");
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))

View File

@ -2,6 +2,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Generic;
namespace MediaBrowser.Model.Playlists namespace MediaBrowser.Model.Playlists
{ {
@ -9,15 +10,10 @@ namespace MediaBrowser.Model.Playlists
{ {
public string Name { get; set; } public string Name { get; set; }
public Guid[] ItemIdList { get; set; } public IReadOnlyList<Guid> ItemIdList { get; set; } = Array.Empty<Guid>();
public string MediaType { get; set; } public string MediaType { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public PlaylistCreationRequest()
{
ItemIdList = Array.Empty<Guid>();
}
} }
} }

View File

@ -38,7 +38,7 @@ namespace MediaBrowser.Providers.Manager
{ {
if (replaceData || string.IsNullOrEmpty(target.Name)) if (replaceData || string.IsNullOrEmpty(target.Name))
{ {
// Safeguard against incoming data having an emtpy name // Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.Name)) if (!string.IsNullOrWhiteSpace(source.Name))
{ {
target.Name = source.Name; target.Name = source.Name;
@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Manager
if (replaceData || string.IsNullOrEmpty(target.OriginalTitle)) if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
{ {
// Safeguard against incoming data having an emtpy name // Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.OriginalTitle)) if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
{ {
target.OriginalTitle = source.OriginalTitle; target.OriginalTitle = source.OriginalTitle;

View File

@ -50,7 +50,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false); var result = await GetRootObject(imdbId, cancellationToken).ConfigureAwait(false);
// Only take the name and rating if the user's language is set to english, since Omdb has no localization // Only take the name and rating if the user's language is set to English, since Omdb has no localization
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport) if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
{ {
item.Name = result.Title; item.Name = result.Title;
@ -151,7 +151,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
return false; return false;
} }
// Only take the name and rating if the user's language is set to english, since Omdb has no localization // Only take the name and rating if the user's language is set to English, since Omdb has no localization
if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport) if (string.Equals(language, "en", StringComparison.OrdinalIgnoreCase) || _configurationManager.Configuration.EnableNewOmdbSupport)
{ {
item.Name = result.Title; item.Name = result.Title;
@ -385,7 +385,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport; var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
// Grab series genres because IMDb data is better than TVDB. Leave movies alone // Grab series genres because IMDb data is better than TVDB. Leave movies alone
// But only do it if english is the preferred language because this data will not be localized // But only do it if English is the preferred language because this data will not be localized
if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre)) if (isConfiguredForEnglish && !string.IsNullOrWhiteSpace(result.Genre))
{ {
item.Genres = Array.Empty<string>(); item.Genres = Array.Empty<string>();
@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
if (isConfiguredForEnglish) if (isConfiguredForEnglish)
{ {
// Omdb is currently english only, so for other languages skip this and let secondary providers fill it in // Omdb is currently English only, so for other languages skip this and let secondary providers fill it in
item.Overview = result.Plot; item.Overview = result.Plot;
} }
@ -455,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{ {
var lang = item.GetPreferredMetadataLanguage(); var lang = item.GetPreferredMetadataLanguage();
// The data isn't localized and so can only be used for english users // The data isn't localized and so can only be used for English users
return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase); return string.Equals(lang, "en", StringComparison.OrdinalIgnoreCase);
} }

View File

@ -1,10 +0,0 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class PluginConfiguration : BasePluginConfiguration
{
}
}

View File

@ -1,29 +0,0 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class Plugin : BasePlugin<PluginConfiguration>
{
public static Plugin Instance { get; private set; }
public override Guid Id => new Guid("a677c0da-fac5-4cde-941a-7134223f14c8");
public override string Name => "TheTVDB";
public override string Description => "Get metadata for movies and other video content from TheTVDB.";
// TODO remove when plugin removed from server.
public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml";
public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
: base(applicationPaths, xmlSerializer)
{
Instance = this;
}
}
}

View File

@ -1,289 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Caching.Memory;
using TvDbSharper;
using TvDbSharper.Dto;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbClientManager
{
private const string DefaultLanguage = "en";
private readonly IMemoryCache _cache;
private readonly TvDbClient _tvDbClient;
private DateTime _tokenCreatedAt;
public TvdbClientManager(IMemoryCache memoryCache)
{
_cache = memoryCache;
_tvDbClient = new TvDbClient();
}
private TvDbClient TvDbClient
{
get
{
if (string.IsNullOrEmpty(_tvDbClient.Authentication.Token))
{
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
_tokenCreatedAt = DateTime.Now;
}
// Refresh if necessary
if (_tokenCreatedAt < DateTime.Now.Subtract(TimeSpan.FromHours(20)))
{
try
{
_tvDbClient.Authentication.RefreshTokenAsync().GetAwaiter().GetResult();
}
catch
{
_tvDbClient.Authentication.AuthenticateAsync(TvdbUtils.TvdbApiKey).GetAwaiter().GetResult();
}
_tokenCreatedAt = DateTime.Now;
}
return _tvDbClient;
}
}
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByNameAsync(string name, string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("series", name, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByNameAsync(name, cancellationToken));
}
public Task<TvDbResponse<Series>> GetSeriesByIdAsync(int tvdbId, string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("series", tvdbId, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetAsync(tvdbId, cancellationToken));
}
public Task<TvDbResponse<EpisodeRecord>> GetEpisodesAsync(int episodeTvdbId, string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("episode", episodeTvdbId, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Episodes.GetAsync(episodeTvdbId, cancellationToken));
}
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByImdbIdAsync(
string imdbId,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("series", imdbId, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByImdbIdAsync(imdbId, cancellationToken));
}
public Task<TvDbResponse<SeriesSearchResult[]>> GetSeriesByZap2ItIdAsync(
string zap2ItId,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("series", zap2ItId, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Search.SearchSeriesByZap2ItIdAsync(zap2ItId, cancellationToken));
}
public Task<TvDbResponse<Actor[]>> GetActorsAsync(
int tvdbId,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("actors", tvdbId, language);
return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetActorsAsync(tvdbId, cancellationToken));
}
public Task<TvDbResponse<Image[]>> GetImagesAsync(
int tvdbId,
ImagesQuery imageQuery,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("images", tvdbId, language, imageQuery);
return TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesAsync(tvdbId, imageQuery, cancellationToken));
}
public Task<TvDbResponse<Language[]>> GetLanguagesAsync(CancellationToken cancellationToken)
{
return TryGetValue("languages", null, () => TvDbClient.Languages.GetAllAsync(cancellationToken));
}
public Task<TvDbResponse<EpisodesSummary>> GetSeriesEpisodeSummaryAsync(
int tvdbId,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey("seriesepisodesummary", tvdbId, language);
return TryGetValue(cacheKey, language,
() => TvDbClient.Series.GetEpisodesSummaryAsync(tvdbId, cancellationToken));
}
public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
int tvdbId,
int page,
EpisodeQuery episodeQuery,
string language,
CancellationToken cancellationToken)
{
var cacheKey = GenerateKey(language, tvdbId, episodeQuery);
return TryGetValue(cacheKey, language,
() => TvDbClient.Series.GetEpisodesAsync(tvdbId, page, episodeQuery, cancellationToken));
}
public Task<string> GetEpisodeTvdbId(
EpisodeInfo searchInfo,
string language,
CancellationToken cancellationToken)
{
searchInfo.SeriesProviderIds.TryGetValue(
nameof(MetadataProvider.Tvdb),
out var seriesTvdbId);
var episodeQuery = new EpisodeQuery();
// Prefer SxE over premiere date as it is more robust
if (searchInfo.IndexNumber.HasValue && searchInfo.ParentIndexNumber.HasValue)
{
switch (searchInfo.SeriesDisplayOrder)
{
case "dvd":
episodeQuery.DvdEpisode = searchInfo.IndexNumber.Value;
episodeQuery.DvdSeason = searchInfo.ParentIndexNumber.Value;
break;
case "absolute":
episodeQuery.AbsoluteNumber = searchInfo.IndexNumber.Value;
break;
default:
// aired order
episodeQuery.AiredEpisode = searchInfo.IndexNumber.Value;
episodeQuery.AiredSeason = searchInfo.ParentIndexNumber.Value;
break;
}
}
else if (searchInfo.PremiereDate.HasValue)
{
// tvdb expects yyyy-mm-dd format
episodeQuery.FirstAired = searchInfo.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
return GetEpisodeTvdbId(Convert.ToInt32(seriesTvdbId, CultureInfo.InvariantCulture), episodeQuery, language, cancellationToken);
}
public async Task<string> GetEpisodeTvdbId(
int seriesTvdbId,
EpisodeQuery episodeQuery,
string language,
CancellationToken cancellationToken)
{
var episodePage =
await GetEpisodesPageAsync(Convert.ToInt32(seriesTvdbId), episodeQuery, language, cancellationToken)
.ConfigureAwait(false);
return episodePage.Data.FirstOrDefault()?.Id.ToString(CultureInfo.InvariantCulture);
}
public Task<TvDbResponse<EpisodeRecord[]>> GetEpisodesPageAsync(
int tvdbId,
EpisodeQuery episodeQuery,
string language,
CancellationToken cancellationToken)
{
return GetEpisodesPageAsync(tvdbId, 1, episodeQuery, language, cancellationToken);
}
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeriesAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
if (imagesSummary.Data.Fanart > 0)
{
yield return KeyType.Fanart;
}
if (imagesSummary.Data.Series > 0)
{
yield return KeyType.Series;
}
if (imagesSummary.Data.Poster > 0)
{
yield return KeyType.Poster;
}
}
public async IAsyncEnumerable<KeyType> GetImageKeyTypesForSeasonAsync(int tvdbId, string language, [EnumeratorCancellation] CancellationToken cancellationToken)
{
var cacheKey = GenerateKey(nameof(TvDbClient.Series.GetImagesSummaryAsync), tvdbId);
var imagesSummary = await TryGetValue(cacheKey, language, () => TvDbClient.Series.GetImagesSummaryAsync(tvdbId, cancellationToken)).ConfigureAwait(false);
if (imagesSummary.Data.Season > 0)
{
yield return KeyType.Season;
}
if (imagesSummary.Data.Fanart > 0)
{
yield return KeyType.Fanart;
}
// TODO seasonwide is not supported in TvDbSharper
}
private async Task<T> TryGetValue<T>(string key, string language, Func<Task<T>> resultFactory)
{
if (_cache.TryGetValue(key, out T cachedValue))
{
return cachedValue;
}
_tvDbClient.AcceptedLanguage = TvdbUtils.NormalizeLanguage(language) ?? DefaultLanguage;
var result = await resultFactory.Invoke().ConfigureAwait(false);
_cache.Set(key, result, TimeSpan.FromHours(1));
return result;
}
private static string GenerateKey(params object[] objects)
{
var key = string.Empty;
foreach (var obj in objects)
{
var objType = obj.GetType();
if (objType.IsPrimitive || objType == typeof(string))
{
key += obj + ";";
}
else
{
foreach (PropertyInfo propertyInfo in objType.GetProperties())
{
var currentValue = propertyInfo.GetValue(obj, null);
if (currentValue == null)
{
continue;
}
key += propertyInfo.Name + "=" + currentValue + ";";
}
}
}
return key;
}
}
}

View File

@ -1,130 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
using TvDbSharper.Dto;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbEpisodeImageProvider : IRemoteImageProvider
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbEpisodeImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeImageProvider> logger, TvdbClientManager tvdbClientManager)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
public string Name => "TheTVDB";
public bool Supports(BaseItem item)
{
return item is Episode;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
{
ImageType.Primary
};
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var episode = (Episode)item;
var series = episode.Series;
var imageResult = new List<RemoteImageInfo>();
var language = item.GetPreferredMetadataLanguage();
if (series != null && TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
{
// Process images
try
{
string episodeTvdbId = null;
if (episode.IndexNumber.HasValue && episode.ParentIndexNumber.HasValue)
{
var episodeInfo = new EpisodeInfo
{
IndexNumber = episode.IndexNumber.Value,
ParentIndexNumber = episode.ParentIndexNumber.Value,
SeriesProviderIds = series.ProviderIds,
SeriesDisplayOrder = series.DisplayOrder
};
episodeTvdbId = await _tvdbClientManager
.GetEpisodeTvdbId(episodeInfo, language, cancellationToken).ConfigureAwait(false);
}
if (string.IsNullOrEmpty(episodeTvdbId))
{
_logger.LogError(
"Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
episode.ParentIndexNumber,
episode.IndexNumber,
series.GetProviderId(MetadataProvider.Tvdb));
return imageResult;
}
var episodeResult =
await _tvdbClientManager
.GetEpisodesAsync(Convert.ToInt32(episodeTvdbId, CultureInfo.InvariantCulture), language, cancellationToken)
.ConfigureAwait(false);
var image = GetImageInfo(episodeResult.Data);
if (image != null)
{
imageResult.Add(image);
}
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve episode images for series {TvDbId}", series.GetProviderId(MetadataProvider.Tvdb));
}
}
return imageResult;
}
private RemoteImageInfo GetImageInfo(EpisodeRecord episode)
{
if (string.IsNullOrEmpty(episode.Filename))
{
return null;
}
return new RemoteImageInfo
{
Width = Convert.ToInt32(episode.ThumbWidth, CultureInfo.InvariantCulture),
Height = Convert.ToInt32(episode.ThumbHeight, CultureInfo.InvariantCulture),
ProviderName = Name,
Url = TvdbUtils.BannerUrl + episode.Filename,
Type = ImageType.Primary
};
}
public int Order => 0;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}

View File

@ -1,262 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
using TvDbSharper.Dto;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
/// <summary>
/// Class RemoteEpisodeProvider.
/// </summary>
public class TvdbEpisodeProvider : IRemoteMetadataProvider<Episode, EpisodeInfo>, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbEpisodeProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbEpisodeProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbEpisodeProvider> logger, TvdbClientManager tvdbClientManager)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
var list = new List<RemoteSearchResult>();
// Either an episode number or date must be provided; and the dictionary of provider ids must be valid
if ((searchInfo.IndexNumber == null && searchInfo.PremiereDate == null)
|| !TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds))
{
return list;
}
var metadataResult = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
if (!metadataResult.HasMetadata)
{
return list;
}
var item = metadataResult.Item;
list.Add(new RemoteSearchResult
{
IndexNumber = item.IndexNumber,
Name = item.Name,
ParentIndexNumber = item.ParentIndexNumber,
PremiereDate = item.PremiereDate,
ProductionYear = item.ProductionYear,
ProviderIds = item.ProviderIds,
SearchProviderName = Name,
IndexNumberEnd = item.IndexNumberEnd
});
return list;
}
public string Name => "TheTVDB";
public async Task<MetadataResult<Episode>> GetMetadata(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
var result = new MetadataResult<Episode>
{
QueriedById = true
};
if (TvdbSeriesProvider.IsValidSeries(searchInfo.SeriesProviderIds) &&
(searchInfo.IndexNumber.HasValue || searchInfo.PremiereDate.HasValue))
{
result = await GetEpisode(searchInfo, cancellationToken).ConfigureAwait(false);
}
else
{
_logger.LogDebug("No series identity found for {EpisodeName}", searchInfo.Name);
}
return result;
}
private async Task<MetadataResult<Episode>> GetEpisode(EpisodeInfo searchInfo, CancellationToken cancellationToken)
{
var result = new MetadataResult<Episode>
{
QueriedById = true
};
string seriesTvdbId = searchInfo.GetProviderId(MetadataProvider.Tvdb);
string episodeTvdbId = null;
try
{
episodeTvdbId = await _tvdbClientManager
.GetEpisodeTvdbId(searchInfo, searchInfo.MetadataLanguage, cancellationToken)
.ConfigureAwait(false);
if (string.IsNullOrEmpty(episodeTvdbId))
{
_logger.LogError(
"Episode {SeasonNumber}x{EpisodeNumber} not found for series {SeriesTvdbId}",
searchInfo.ParentIndexNumber, searchInfo.IndexNumber, seriesTvdbId);
return result;
}
var episodeResult = await _tvdbClientManager.GetEpisodesAsync(
Convert.ToInt32(episodeTvdbId), searchInfo.MetadataLanguage,
cancellationToken).ConfigureAwait(false);
result = MapEpisodeToResult(searchInfo, episodeResult.Data);
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve episode with id {EpisodeTvDbId}, series id {SeriesTvdbId}", episodeTvdbId, seriesTvdbId);
}
return result;
}
private static MetadataResult<Episode> MapEpisodeToResult(EpisodeInfo id, EpisodeRecord episode)
{
var result = new MetadataResult<Episode>
{
HasMetadata = true,
Item = new Episode
{
IndexNumber = id.IndexNumber,
ParentIndexNumber = id.ParentIndexNumber,
IndexNumberEnd = id.IndexNumberEnd,
AirsBeforeEpisodeNumber = episode.AirsBeforeEpisode,
AirsAfterSeasonNumber = episode.AirsAfterSeason,
AirsBeforeSeasonNumber = episode.AirsBeforeSeason,
Name = episode.EpisodeName,
Overview = episode.Overview,
CommunityRating = (float?)episode.SiteRating,
OfficialRating = episode.ContentRating,
}
};
result.ResetPeople();
var item = result.Item;
item.SetProviderId(MetadataProvider.Tvdb, episode.Id.ToString());
item.SetProviderId(MetadataProvider.Imdb, episode.ImdbId);
if (string.Equals(id.SeriesDisplayOrder, "dvd", StringComparison.OrdinalIgnoreCase))
{
item.IndexNumber = Convert.ToInt32(episode.DvdEpisodeNumber ?? episode.AiredEpisodeNumber);
item.ParentIndexNumber = episode.DvdSeason ?? episode.AiredSeason;
}
else if (string.Equals(id.SeriesDisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase))
{
if (episode.AbsoluteNumber.GetValueOrDefault() != 0)
{
item.IndexNumber = episode.AbsoluteNumber;
}
}
else if (episode.AiredEpisodeNumber.HasValue)
{
item.IndexNumber = episode.AiredEpisodeNumber;
}
else if (episode.AiredSeason.HasValue)
{
item.ParentIndexNumber = episode.AiredSeason;
}
if (DateTime.TryParse(episode.FirstAired, out var date))
{
// dates from tvdb are UTC but without offset or Z
item.PremiereDate = date;
item.ProductionYear = date.Year;
}
foreach (var director in episode.Directors)
{
result.AddPerson(new PersonInfo
{
Name = director,
Type = PersonType.Director
});
}
// GuestStars is a weird list of names and roles
// Example:
// 1: Some Actor (Role1
// 2: Role2
// 3: Role3)
// 4: Another Actor (Role1
// ...
for (var i = 0; i < episode.GuestStars.Length; ++i)
{
var currentActor = episode.GuestStars[i];
var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
if (roleStartIndex == -1)
{
result.AddPerson(new PersonInfo
{
Type = PersonType.GuestStar,
Name = currentActor,
Role = string.Empty
});
continue;
}
var roles = new List<string> { currentActor.Substring(roleStartIndex + 1) };
// Fetch all roles
for (var j = i + 1; j < episode.GuestStars.Length; ++j)
{
var currentRole = episode.GuestStars[j];
var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
if (roleEndIndex == -1)
{
roles.Add(currentRole);
continue;
}
roles.Add(currentRole.TrimEnd(')'));
// Update the outer index (keep in mind it adds 1 after the iteration)
i = j;
break;
}
result.AddPerson(new PersonInfo
{
Type = PersonType.GuestStar,
Name = currentActor.Substring(0, roleStartIndex).Trim(),
Role = string.Join(", ", roles)
});
}
foreach (var writer in episode.Writers)
{
result.AddPerson(new PersonInfo
{
Name = writer,
Type = PersonType.Writer
});
}
result.ResultLanguage = episode.Language.EpisodeName;
return result;
}
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
public int Order => 0;
}
}

View File

@ -1,113 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbPersonImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbPersonImageProvider> _logger;
private readonly ILibraryManager _libraryManager;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbPersonImageProvider(ILibraryManager libraryManager, IHttpClientFactory httpClientFactory, ILogger<TvdbPersonImageProvider> logger, TvdbClientManager tvdbClientManager)
{
_libraryManager = libraryManager;
_httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
/// <inheritdoc />
public string Name => "TheTVDB";
/// <inheritdoc />
public int Order => 1;
/// <inheritdoc />
public bool Supports(BaseItem item) => item is Person;
/// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
yield return ImageType.Primary;
}
/// <inheritdoc />
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var seriesWithPerson = _libraryManager.GetItemList(new InternalItemsQuery
{
IncludeItemTypes = new[] { nameof(Series) },
PersonIds = new[] { item.Id },
DtoOptions = new DtoOptions(false)
{
EnableImages = false
}
}).Cast<Series>()
.Where(i => TvdbSeriesProvider.IsValidSeries(i.ProviderIds))
.ToList();
var infos = (await Task.WhenAll(seriesWithPerson.Select(async i =>
await GetImageFromSeriesData(i, item.Name, cancellationToken).ConfigureAwait(false)))
.ConfigureAwait(false))
.Where(i => i != null)
.Take(1);
return infos;
}
private async Task<RemoteImageInfo> GetImageFromSeriesData(Series series, string personName, CancellationToken cancellationToken)
{
var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb));
try
{
var actorsResult = await _tvdbClientManager
.GetActorsAsync(tvdbId, series.GetPreferredMetadataLanguage(), cancellationToken)
.ConfigureAwait(false);
var actor = actorsResult.Data.FirstOrDefault(a =>
string.Equals(a.Name, personName, StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(a.Image));
if (actor == null)
{
return null;
}
return new RemoteImageInfo
{
Url = TvdbUtils.BannerUrl + actor.Image,
Type = ImageType.Primary,
ProviderName = Name
};
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve actor {ActorName} from series {SeriesTvdbId}", personName, tvdbId);
return null;
}
}
/// <inheritdoc />
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}

View File

@ -1,155 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
using TvDbSharper.Dto;
using RatingType = MediaBrowser.Model.Dto.RatingType;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeasonImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbSeasonImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeasonImageProvider> logger, TvdbClientManager tvdbClientManager)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
public string Name => ProviderName;
public static string ProviderName => "TheTVDB";
public bool Supports(BaseItem item)
{
return item is Season;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
{
ImageType.Primary,
ImageType.Banner,
ImageType.Backdrop
};
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
var season = (Season)item;
var series = season.Series;
if (series == null || !season.IndexNumber.HasValue || !TvdbSeriesProvider.IsValidSeries(series.ProviderIds))
{
return Array.Empty<RemoteImageInfo>();
}
var tvdbId = Convert.ToInt32(series.GetProviderId(MetadataProvider.Tvdb));
var seasonNumber = season.IndexNumber.Value;
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
var keyTypes = _tvdbClientManager.GetImageKeyTypesForSeasonAsync(tvdbId, language, cancellationToken).ConfigureAwait(false);
await foreach (var keyType in keyTypes)
{
var imageQuery = new ImagesQuery
{
KeyType = keyType,
SubKey = seasonNumber.ToString()
};
try
{
var imageResults = await _tvdbClientManager
.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken).ConfigureAwait(false);
remoteImages.AddRange(GetImages(imageResults.Data, language));
}
catch (TvDbServerException)
{
_logger.LogDebug("No images of type {KeyType} found for series {TvdbId}", keyType, tvdbId);
}
}
return remoteImages;
}
private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
{
var list = new List<RemoteImageInfo>();
// any languages with null ids are ignored
var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data.Where(x => x.Id.HasValue);
foreach (Image image in images)
{
var imageInfo = new RemoteImageInfo
{
RatingType = RatingType.Score,
CommunityRating = (double?)image.RatingsInfo.Average,
VoteCount = image.RatingsInfo.Count,
Url = TvdbUtils.BannerUrl + image.FileName,
ProviderName = ProviderName,
Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
};
var resolution = image.Resolution.Split('x');
if (resolution.Length == 2)
{
imageInfo.Width = Convert.ToInt32(resolution[0]);
imageInfo.Height = Convert.ToInt32(resolution[1]);
}
imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
list.Add(imageInfo);
}
var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
return list.OrderByDescending(i =>
{
if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (!isLanguageEn)
{
if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
}
if (string.IsNullOrEmpty(i.Language))
{
return isLanguageEn ? 3 : 2;
}
return 0;
})
.ThenByDescending(i => i.CommunityRating ?? 0)
.ThenByDescending(i => i.VoteCount ?? 0);
}
public int Order => 0;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}

View File

@ -1,153 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
using TvDbSharper.Dto;
using RatingType = MediaBrowser.Model.Dto.RatingType;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbSeriesImageProvider : IRemoteImageProvider, IHasOrder
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeriesImageProvider> _logger;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbSeriesImageProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesImageProvider> logger, TvdbClientManager tvdbClientManager)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_tvdbClientManager = tvdbClientManager;
}
public string Name => ProviderName;
public static string ProviderName => "TheTVDB";
public bool Supports(BaseItem item)
{
return item is Series;
}
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return new List<ImageType>
{
ImageType.Primary,
ImageType.Banner,
ImageType.Backdrop
};
}
public async Task<IEnumerable<RemoteImageInfo>> GetImages(BaseItem item, CancellationToken cancellationToken)
{
if (!TvdbSeriesProvider.IsValidSeries(item.ProviderIds))
{
return Array.Empty<RemoteImageInfo>();
}
var language = item.GetPreferredMetadataLanguage();
var remoteImages = new List<RemoteImageInfo>();
var tvdbId = Convert.ToInt32(item.GetProviderId(MetadataProvider.Tvdb));
var allowedKeyTypes = _tvdbClientManager.GetImageKeyTypesForSeriesAsync(tvdbId, language, cancellationToken)
.ConfigureAwait(false);
await foreach (KeyType keyType in allowedKeyTypes)
{
var imageQuery = new ImagesQuery
{
KeyType = keyType
};
try
{
var imageResults =
await _tvdbClientManager.GetImagesAsync(tvdbId, imageQuery, language, cancellationToken)
.ConfigureAwait(false);
remoteImages.AddRange(GetImages(imageResults.Data, language));
}
catch (TvDbServerException)
{
_logger.LogDebug("No images of type {KeyType} exist for series {TvDbId}", keyType,
tvdbId);
}
}
return remoteImages;
}
private IEnumerable<RemoteImageInfo> GetImages(Image[] images, string preferredLanguage)
{
var list = new List<RemoteImageInfo>();
var languages = _tvdbClientManager.GetLanguagesAsync(CancellationToken.None).Result.Data;
foreach (Image image in images)
{
var imageInfo = new RemoteImageInfo
{
RatingType = RatingType.Score,
CommunityRating = (double?)image.RatingsInfo.Average,
VoteCount = image.RatingsInfo.Count,
Url = TvdbUtils.BannerUrl + image.FileName,
ProviderName = Name,
Language = languages.FirstOrDefault(lang => lang.Id == image.LanguageId)?.Abbreviation,
ThumbnailUrl = TvdbUtils.BannerUrl + image.Thumbnail
};
var resolution = image.Resolution.Split('x');
if (resolution.Length == 2)
{
imageInfo.Width = Convert.ToInt32(resolution[0]);
imageInfo.Height = Convert.ToInt32(resolution[1]);
}
imageInfo.Type = TvdbUtils.GetImageTypeFromKeyType(image.KeyType);
list.Add(imageInfo);
}
var isLanguageEn = string.Equals(preferredLanguage, "en", StringComparison.OrdinalIgnoreCase);
return list.OrderByDescending(i =>
{
if (string.Equals(preferredLanguage, i.Language, StringComparison.OrdinalIgnoreCase))
{
return 3;
}
if (!isLanguageEn)
{
if (string.Equals("en", i.Language, StringComparison.OrdinalIgnoreCase))
{
return 2;
}
}
if (string.IsNullOrEmpty(i.Language))
{
return isLanguageEn ? 3 : 2;
}
return 0;
})
.ThenByDescending(i => i.CommunityRating ?? 0)
.ThenByDescending(i => i.VoteCount ?? 0);
}
public int Order => 0;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}

View File

@ -1,419 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
using TvDbSharper;
using TvDbSharper.Dto;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Providers.Plugins.TheTvdb
{
public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
{
internal static TvdbSeriesProvider Current { get; private set; }
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<TvdbSeriesProvider> _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localizationManager;
private readonly TvdbClientManager _tvdbClientManager;
public TvdbSeriesProvider(IHttpClientFactory httpClientFactory, ILogger<TvdbSeriesProvider> logger, ILibraryManager libraryManager, ILocalizationManager localizationManager, TvdbClientManager tvdbClientManager)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_libraryManager = libraryManager;
_localizationManager = localizationManager;
Current = this;
_tvdbClientManager = tvdbClientManager;
}
public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(SeriesInfo searchInfo, CancellationToken cancellationToken)
{
if (IsValidSeries(searchInfo.ProviderIds))
{
var metadata = await GetMetadata(searchInfo, cancellationToken).ConfigureAwait(false);
if (metadata.HasMetadata)
{
return new List<RemoteSearchResult>
{
new RemoteSearchResult
{
Name = metadata.Item.Name,
PremiereDate = metadata.Item.PremiereDate,
ProductionYear = metadata.Item.ProductionYear,
ProviderIds = metadata.Item.ProviderIds,
SearchProviderName = Name
}
};
}
}
return await FindSeries(searchInfo.Name, searchInfo.Year, searchInfo.MetadataLanguage, cancellationToken).ConfigureAwait(false);
}
public async Task<MetadataResult<Series>> GetMetadata(SeriesInfo itemId, CancellationToken cancellationToken)
{
var result = new MetadataResult<Series>
{
QueriedById = true
};
if (!IsValidSeries(itemId.ProviderIds))
{
result.QueriedById = false;
await Identify(itemId).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
if (IsValidSeries(itemId.ProviderIds))
{
result.Item = new Series();
result.HasMetadata = true;
await FetchSeriesData(result, itemId.MetadataLanguage, itemId.ProviderIds, cancellationToken)
.ConfigureAwait(false);
}
return result;
}
private async Task FetchSeriesData(MetadataResult<Series> result, string metadataLanguage, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
{
var series = result.Item;
if (seriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var tvdbId) && !string.IsNullOrEmpty(tvdbId))
{
series.SetProviderId(MetadataProvider.Tvdb, tvdbId);
}
if (seriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out var imdbId) && !string.IsNullOrEmpty(imdbId))
{
series.SetProviderId(MetadataProvider.Imdb, imdbId);
tvdbId = await GetSeriesByRemoteId(imdbId, MetadataProvider.Imdb.ToString(), metadataLanguage,
cancellationToken).ConfigureAwait(false);
}
if (seriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out var zap2It) && !string.IsNullOrEmpty(zap2It))
{
series.SetProviderId(MetadataProvider.Zap2It, zap2It);
tvdbId = await GetSeriesByRemoteId(zap2It, MetadataProvider.Zap2It.ToString(), metadataLanguage,
cancellationToken).ConfigureAwait(false);
}
try
{
var seriesResult =
await _tvdbClientManager
.GetSeriesByIdAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken)
.ConfigureAwait(false);
await MapSeriesToResult(result, seriesResult.Data, metadataLanguage).ConfigureAwait(false);
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve series with id {TvdbId}", tvdbId);
return;
}
cancellationToken.ThrowIfCancellationRequested();
result.ResetPeople();
try
{
var actorsResult = await _tvdbClientManager
.GetActorsAsync(Convert.ToInt32(tvdbId), metadataLanguage, cancellationToken).ConfigureAwait(false);
MapActorsToResult(result, actorsResult.Data);
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve actors for series {TvdbId}", tvdbId);
}
}
private async Task<string> GetSeriesByRemoteId(string id, string idType, string language, CancellationToken cancellationToken)
{
TvDbResponse<SeriesSearchResult[]> result = null;
try
{
if (string.Equals(idType, MetadataProvider.Zap2It.ToString(), StringComparison.OrdinalIgnoreCase))
{
result = await _tvdbClientManager.GetSeriesByZap2ItIdAsync(id, language, cancellationToken)
.ConfigureAwait(false);
}
else
{
result = await _tvdbClientManager.GetSeriesByImdbIdAsync(id, language, cancellationToken)
.ConfigureAwait(false);
}
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to retrieve series with remote id {RemoteId}", id);
}
return result?.Data[0].Id.ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Check whether a dictionary of provider IDs includes an entry for a valid TV metadata provider.
/// </summary>
/// <param name="seriesProviderIds">The dictionary to check.</param>
/// <returns>True, if the dictionary contains a valid TV provider ID, otherwise false.</returns>
internal static bool IsValidSeries(Dictionary<string, string> seriesProviderIds)
{
return seriesProviderIds.ContainsKey(MetadataProvider.Tvdb.ToString()) ||
seriesProviderIds.ContainsKey(MetadataProvider.Imdb.ToString()) ||
seriesProviderIds.ContainsKey(MetadataProvider.Zap2It.ToString());
}
/// <summary>
/// Finds the series.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="year">The year.</param>
/// <param name="language">The language.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{System.String}.</returns>
private async Task<IEnumerable<RemoteSearchResult>> FindSeries(string name, int? year, string language, CancellationToken cancellationToken)
{
var results = await FindSeriesInternal(name, language, cancellationToken).ConfigureAwait(false);
if (results.Count == 0)
{
var parsedName = _libraryManager.ParseName(name);
var nameWithoutYear = parsedName.Name;
if (!string.IsNullOrWhiteSpace(nameWithoutYear) && !string.Equals(nameWithoutYear, name, StringComparison.OrdinalIgnoreCase))
{
results = await FindSeriesInternal(nameWithoutYear, language, cancellationToken).ConfigureAwait(false);
}
}
return results.Where(i =>
{
if (year.HasValue && i.ProductionYear.HasValue)
{
// Allow one year tolerance
return Math.Abs(year.Value - i.ProductionYear.Value) <= 1;
}
return true;
});
}
private async Task<List<RemoteSearchResult>> FindSeriesInternal(string name, string language, CancellationToken cancellationToken)
{
var comparableName = GetComparableName(name);
var list = new List<Tuple<List<string>, RemoteSearchResult>>();
TvDbResponse<SeriesSearchResult[]> result;
try
{
result = await _tvdbClientManager.GetSeriesByNameAsync(comparableName, language, cancellationToken)
.ConfigureAwait(false);
}
catch (TvDbServerException e)
{
_logger.LogError(e, "No series results found for {Name}", comparableName);
return new List<RemoteSearchResult>();
}
foreach (var seriesSearchResult in result.Data)
{
var tvdbTitles = new List<string>
{
GetComparableName(seriesSearchResult.SeriesName)
};
tvdbTitles.AddRange(seriesSearchResult.Aliases.Select(GetComparableName));
DateTime.TryParse(seriesSearchResult.FirstAired, out var firstAired);
var remoteSearchResult = new RemoteSearchResult
{
Name = tvdbTitles.FirstOrDefault(),
ProductionYear = firstAired.Year,
SearchProviderName = Name
};
if (!string.IsNullOrEmpty(seriesSearchResult.Banner))
{
// Results from their Search endpoints already include the /banners/ part in the url, because reasons...
remoteSearchResult.ImageUrl = TvdbUtils.TvdbImageBaseUrl + seriesSearchResult.Banner;
}
try
{
var seriesSesult =
await _tvdbClientManager.GetSeriesByIdAsync(seriesSearchResult.Id, language, cancellationToken)
.ConfigureAwait(false);
remoteSearchResult.SetProviderId(MetadataProvider.Imdb, seriesSesult.Data.ImdbId);
remoteSearchResult.SetProviderId(MetadataProvider.Zap2It, seriesSesult.Data.Zap2itId);
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Unable to retrieve series with id {TvdbId}", seriesSearchResult.Id);
}
remoteSearchResult.SetProviderId(MetadataProvider.Tvdb, seriesSearchResult.Id.ToString());
list.Add(new Tuple<List<string>, RemoteSearchResult>(tvdbTitles, remoteSearchResult));
}
return list
.OrderBy(i => i.Item1.Contains(comparableName, StringComparer.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(i => list.IndexOf(i))
.Select(i => i.Item2)
.ToList();
}
/// <summary>
/// Gets the name of the comparable.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>System.String.</returns>
private string GetComparableName(string name)
{
name = name.ToLowerInvariant();
name = name.Normalize(NormalizationForm.FormKD);
name = name.Replace(", the", string.Empty).Replace("the ", " ").Replace(" the ", " ");
name = name.Replace("&", " and " );
name = Regex.Replace(name, @"[\p{Lm}\p{Mn}]", string.Empty); // Remove diacritics, etc
name = Regex.Replace(name, @"[\W\p{Pc}]+", " "); // Replace sequences of non-word characters and _ with " "
return name.Trim();
}
private async Task MapSeriesToResult(MetadataResult<Series> result, TvDbSharper.Dto.Series tvdbSeries, string metadataLanguage)
{
Series series = result.Item;
series.SetProviderId(MetadataProvider.Tvdb, tvdbSeries.Id.ToString());
series.Name = tvdbSeries.SeriesName;
series.Overview = (tvdbSeries.Overview ?? string.Empty).Trim();
result.ResultLanguage = metadataLanguage;
series.AirDays = TVUtils.GetAirDays(tvdbSeries.AirsDayOfWeek);
series.AirTime = tvdbSeries.AirsTime;
series.CommunityRating = (float?)tvdbSeries.SiteRating;
series.SetProviderId(MetadataProvider.Imdb, tvdbSeries.ImdbId);
series.SetProviderId(MetadataProvider.Zap2It, tvdbSeries.Zap2itId);
if (Enum.TryParse(tvdbSeries.Status, true, out SeriesStatus seriesStatus))
{
series.Status = seriesStatus;
}
if (DateTime.TryParse(tvdbSeries.FirstAired, out var date))
{
// dates from tvdb are UTC but without offset or Z
series.PremiereDate = date;
series.ProductionYear = date.Year;
}
if (!string.IsNullOrEmpty(tvdbSeries.Runtime) && double.TryParse(tvdbSeries.Runtime, out double runtime))
{
series.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
foreach (var genre in tvdbSeries.Genre)
{
series.AddGenre(genre);
}
if (!string.IsNullOrEmpty(tvdbSeries.Network))
{
series.AddStudio(tvdbSeries.Network);
}
if (result.Item.Status.HasValue && result.Item.Status.Value == SeriesStatus.Ended)
{
try
{
var episodeSummary = await _tvdbClientManager.GetSeriesEpisodeSummaryAsync(tvdbSeries.Id, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
if (episodeSummary.Data.AiredSeasons.Length != 0)
{
var maxSeasonNumber = episodeSummary.Data.AiredSeasons.Max(s => Convert.ToInt32(s, CultureInfo.InvariantCulture));
var episodeQuery = new EpisodeQuery
{
AiredSeason = maxSeasonNumber
};
var episodesPage = await _tvdbClientManager.GetEpisodesPageAsync(tvdbSeries.Id, episodeQuery, metadataLanguage, CancellationToken.None).ConfigureAwait(false);
result.Item.EndDate = episodesPage.Data
.Select(e => DateTime.TryParse(e.FirstAired, out var firstAired) ? firstAired : (DateTime?)null)
.Max();
}
}
catch (TvDbServerException e)
{
_logger.LogError(e, "Failed to find series end date for series {TvdbId}", tvdbSeries.Id);
}
}
}
private static void MapActorsToResult(MetadataResult<Series> result, IEnumerable<Actor> actors)
{
foreach (Actor actor in actors)
{
var personInfo = new PersonInfo
{
Type = PersonType.Actor,
Name = (actor.Name ?? string.Empty).Trim(),
Role = actor.Role,
SortOrder = actor.SortOrder
};
if (!string.IsNullOrEmpty(actor.Image))
{
personInfo.ImageUrl = TvdbUtils.BannerUrl + actor.Image;
}
if (!string.IsNullOrWhiteSpace(personInfo.Name))
{
result.AddPerson(personInfo);
}
}
}
public string Name => "TheTVDB";
public async Task Identify(SeriesInfo info)
{
if (!string.IsNullOrWhiteSpace(info.GetProviderId(MetadataProvider.Tvdb)))
{
return;
}
var srch = await FindSeries(info.Name, info.Year, info.MetadataLanguage, CancellationToken.None)
.ConfigureAwait(false);
var entry = srch.FirstOrDefault();
if (entry != null)
{
var id = entry.GetProviderId(MetadataProvider.Tvdb);
info.SetProviderId(MetadataProvider.Tvdb, id);
}
}
public int Order => 0;
public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken)
{
return _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(url, cancellationToken);
}
}
}

Some files were not shown because too many files have changed in this diff Show More