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
- task: UseDotNet@2
displayName: "Install .NET Core SDK 2.1"
displayName: "Install .NET SDK 5.x"
condition: eq(variables['ImageName'], 'ubuntu-latest')
inputs:
packageType: sdk
version: '2.1.805'
version: '5.x'
- task: UseDotNet@2
displayName: "Update DotNet"

View file

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

View file

@ -2,8 +2,14 @@
namespace Emby.Dlna.Configuration
{
/// <summary>
/// The DlnaOptions class contains the user definable parameters for the dlna subsystems.
/// </summary>
public class DlnaOptions
{
/// <summary>
/// Initializes a new instance of the <see cref="DlnaOptions"/> class.
/// </summary>
public DlnaOptions()
{
EnablePlayTo = true;
@ -11,23 +17,76 @@ namespace Emby.Dlna.Configuration
BlastAliveMessages = true;
SendOnlyMatchedHost = true;
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; }
/// <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; }
/// <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 BlastAliveMessages { get; set; }
public bool SendOnlyMatchedHost { get; set; }
/// <summary>
/// Gets or sets a value indicating whether whether detailed playTo debug logs are sent to the console/log.
/// 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 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; }
/// <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
{
/// <summary>
/// Defines the <see cref="ConnectionManagerService" />.
/// </summary>
public class ConnectionManagerService : BaseService, IConnectionManager
{
private readonly IDlnaManager _dlna;
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(
IDlnaManager dlna,
IServerConfigurationManager config,
@ -28,7 +38,7 @@ namespace Emby.Dlna.ConnectionManager
/// <inheritdoc />
public string GetServiceXml()
{
return new ConnectionManagerXmlBuilder().GetXml();
return ConnectionManagerXmlBuilder.GetXml();
}
/// <inheritdoc />

View file

@ -6,45 +6,57 @@ using Emby.Dlna.Service;
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()
{
var list = new List<StateVariable>();
list.Add(new StateVariable
var list = new List<StateVariable>
{
Name = "SourceProtocolInfo",
DataType = "string",
SendsEvents = true
});
new StateVariable
{
Name = "SourceProtocolInfo",
DataType = "string",
SendsEvents = true
},
list.Add(new StateVariable
{
Name = "SinkProtocolInfo",
DataType = "string",
SendsEvents = true
});
new StateVariable
{
Name = "SinkProtocolInfo",
DataType = "string",
SendsEvents = true
},
list.Add(new StateVariable
{
Name = "CurrentConnectionIDs",
DataType = "string",
SendsEvents = true
});
new StateVariable
{
Name = "CurrentConnectionIDs",
DataType = "string",
SendsEvents = true
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ConnectionStatus",
DataType = "string",
SendsEvents = false,
new StateVariable
{
Name = "A_ARG_TYPE_ConnectionStatus",
DataType = "string",
SendsEvents = false,
AllowedValues = new[]
AllowedValues = new[]
{
"OK",
"ContentFormatMismatch",
@ -52,55 +64,56 @@ namespace Emby.Dlna.ConnectionManager
"UnreliableChannel",
"Unknown"
}
});
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ConnectionManager",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_ConnectionManager",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_Direction",
DataType = "string",
SendsEvents = false,
new StateVariable
{
Name = "A_ARG_TYPE_Direction",
DataType = "string",
SendsEvents = false,
AllowedValues = new[]
AllowedValues = new[]
{
"Output",
"Input"
}
});
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ProtocolInfo",
DataType = "string",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_ProtocolInfo",
DataType = "string",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_ConnectionID",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_ConnectionID",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_AVTransportID",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_AVTransportID",
DataType = "ui4",
SendsEvents = false
},
list.Add(new StateVariable
{
Name = "A_ARG_TYPE_RcsID",
DataType = "ui4",
SendsEvents = false
});
new StateVariable
{
Name = "A_ARG_TYPE_RcsID",
DataType = "ui4",
SendsEvents = false
}
};
return list;
}

View file

@ -11,10 +11,19 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.ConnectionManager
{
/// <summary>
/// Defines the <see cref="ControlHandler" />.
/// </summary>
public class ControlHandler : BaseControlHandler
{
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)
: base(config, logger)
{
@ -33,6 +42,10 @@ namespace Emby.Dlna.ConnectionManager
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)
{
xmlWriter.WriteElementString("Source", _profile.ProtocolInfo);

View file

@ -5,9 +5,16 @@ using Emby.Dlna.Common;
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>
{
@ -21,6 +28,10 @@ namespace Emby.Dlna.ConnectionManager
return list;
}
/// <summary>
/// Returns the action details for "PrepareForConnection".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction PrepareForConnection()
{
var action = new ServiceAction
@ -80,6 +91,10 @@ namespace Emby.Dlna.ConnectionManager
return action;
}
/// <summary>
/// Returns the action details for "GetCurrentConnectionInfo".
/// </summary>
/// <returns>The <see cref="ServiceAction"/>.</returns>
private static ServiceAction GetCurrentConnectionInfo()
{
var action = new ServiceAction
@ -146,7 +161,11 @@ namespace Emby.Dlna.ConnectionManager
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
{
@ -170,7 +189,11 @@ namespace Emby.Dlna.ConnectionManager
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
{
@ -187,7 +210,11 @@ namespace Emby.Dlna.ConnectionManager
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
{

View file

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

View file

@ -484,10 +484,10 @@ namespace Emby.Dlna
/// <summary>
/// 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>
/// <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)
{
if (profile.GetType() == typeof(DeviceProfile))

View file

@ -480,7 +480,7 @@ namespace Emby.Dlna.PlayTo
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)
{
RestartTimerInactive();
@ -775,7 +775,7 @@ namespace Emby.Dlna.PlayTo
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);
}
@ -812,7 +812,7 @@ namespace Emby.Dlna.PlayTo
private XElement ParseResponse(string xml)
{
// Handle different variations sent back by devices
// Handle different variations sent back by devices.
try
{
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
{
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)
{
// 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);

View file

@ -30,7 +30,7 @@ namespace Emby.Naming.Video
/// </summary>
/// <param name="files">List of related video files.</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)
{
var videoResolver = new VideoResolver(_options);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -101,7 +101,7 @@ namespace Emby.Server.Implementations.Library
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)
.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))
{
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer);
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config);
}
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);

View file

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

View file

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

View file

@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (string.IsNullOrWhiteSpace(numberString))
{
// 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
// #EXTINF:0,84. VOX Schweiz
// #EXTINF:0,84.0 - VOX Schweiz

View file

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

View file

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

View file

@ -115,5 +115,8 @@
"TaskRefreshChannels": "Csatornák frissítése",
"TaskCleanTranscodeDescription": "Törli az egy napnál régebbi átkódolási fájlokat.",
"TaskCleanActivityLogDescription": "A beállítottnál korábbi bejegyzések törlése a tevékenységnaplóból.",
"TaskCleanActivityLog": "Tevékenységnapló törlése"
"TaskCleanActivityLog": "Tevékenységnapló törlése",
"Undefined": "Meghatározatlan",
"Forced": "Kényszerített",
"Default": "Alapértelmezett"
}

View file

@ -113,5 +113,10 @@
"TasksChannelsCategory": "Kanały internetowe",
"TasksApplicationCategory": "Aplikacja",
"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)
.ConfigureAwait(false);
if (options.ItemIdList.Length > 0)
if (options.ItemIdList.Count > 0)
{
await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false)
{
@ -184,7 +184,7 @@ namespace Emby.Server.Implementations.Playlists
return Playlist.GetPlaylistItems(playlistMediaType, items, user, options);
}
public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId)
public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
{
var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId);
@ -194,7 +194,7 @@ namespace Emby.Server.Implementations.Playlists
});
}
private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options)
private async Task AddToPlaylistInternal(Guid playlistId, IReadOnlyCollection<Guid> newItemIds, User user, DtoOptions options)
{
// Retrieve the existing playlist
var playlist = _libraryManager.GetItemById(playlistId) as Playlist

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@ -41,6 +42,9 @@ namespace Jellyfin.Api.Controllers
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DynamicHlsController : BaseJellyfinApiController
{
private const string DefaultEncoderPreset = "veryfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager;
@ -56,8 +60,7 @@ namespace Jellyfin.Api.Controllers
private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper;
private readonly DynamicHlsHelper _dynamicHlsHelper;
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Hls;
private readonly EncodingOptions _encodingOptions;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsController"/> class.
@ -92,6 +95,8 @@ namespace Jellyfin.Api.Controllers
ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper)
{
_encodingHelper = new EncodingHelper(mediaEncoder, fileSystem, subtitleEncoder, configuration);
_libraryManager = libraryManager;
_userManager = userManager;
_dlnaManager = dlnaManager;
@ -106,8 +111,7 @@ namespace Jellyfin.Api.Controllers
_transcodingJobHelper = transcodingJobHelper;
_logger = logger;
_dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
_encodingOptions = serverConfigurationManager.GetEncodingOptions();
}
/// <summary>
@ -120,7 +124,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -149,7 +153,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -272,7 +276,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@ -285,7 +289,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -315,7 +319,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -439,7 +443,7 @@ namespace Jellyfin.Api.Controllers
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
};
return await _dynamicHlsHelper.GetMasterHlsPlaylist(_transcodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
}
/// <summary>
@ -452,7 +456,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -481,7 +485,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -615,7 +619,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -645,7 +649,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -812,7 +816,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -953,7 +957,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
/// <param name="playSessionId">The play session id.</param>
/// <param name="segmentContainer">The segment container.</param>
/// <param name="segmentLength">The segment lenght.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="minSegments">The minimum number of segments.</param>
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
@ -983,7 +987,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -1129,7 +1133,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@ -1137,11 +1141,19 @@ namespace Jellyfin.Api.Controllers
var segmentLengths = GetSegmentLengths(state);
var segmentContainer = state.Request.SegmentContainer ?? "ts";
// http://ffmpeg.org/ffmpeg-all.html#toc-hls-2
var isHlsInFmp4 = string.Equals(segmentContainer, "mp4", StringComparison.OrdinalIgnoreCase);
var hlsVersion = isHlsInFmp4 ? "7" : "3";
var builder = new StringBuilder();
builder.AppendLine("#EXTM3U")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
.AppendLine("#EXT-X-VERSION:3")
.Append("#EXT-X-VERSION:")
.Append(hlsVersion)
.AppendLine()
.Append("#EXT-X-TARGETDURATION:")
.Append(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength))
.AppendLine()
@ -1151,6 +1163,18 @@ namespace Jellyfin.Api.Controllers
var segmentExtension = GetSegmentFileExtension(streamingRequest.SegmentContainer);
var queryString = Request.QueryString;
if (isHlsInFmp4)
{
builder.Append("#EXT-X-MAP:URI=\"")
.Append("hls1/")
.Append(name)
.Append("/-1")
.Append(segmentExtension)
.Append(queryString)
.Append('"')
.AppendLine();
}
foreach (var length in segmentLengths)
{
builder.Append("#EXTINF:")
@ -1194,7 +1218,7 @@ namespace Jellyfin.Api.Controllers
_dlnaManager,
_deviceManager,
_transcodingJobHelper,
_transcodingJobType,
TranscodingJobType,
cancellationTokenSource.Token)
.ConfigureAwait(false);
@ -1208,7 +1232,7 @@ namespace Jellyfin.Api.Controllers
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
_logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@ -1222,7 +1246,7 @@ namespace Jellyfin.Api.Controllers
{
if (System.IO.File.Exists(segmentPath))
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release();
released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
@ -1233,7 +1257,13 @@ namespace Jellyfin.Api.Controllers
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
if (currentTranscodingIndex == null)
if (segmentId == -1)
{
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
startTranscoding = true;
segmentId = 0;
}
else if (currentTranscodingIndex == null)
{
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
startTranscoding = true;
@ -1265,13 +1295,12 @@ namespace Jellyfin.Api.Controllers
streamingRequest.StartTimeTicks = GetStartPositionTicks(state, segmentId);
state.WaitForPath = segmentPath;
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
job = await _transcodingJobHelper.StartFfMpeg(
state,
playlistPath,
GetCommandLineArguments(playlistPath, encodingOptions, state, true, segmentId),
GetCommandLineArguments(playlistPath, state, true, segmentId),
Request,
_transcodingJobType,
TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false);
}
catch
@ -1284,7 +1313,7 @@ namespace Jellyfin.Api.Controllers
}
else
{
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler != null)
{
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@ -1301,7 +1330,7 @@ namespace Jellyfin.Api.Controllers
}
_logger.LogDebug("returning {0} [general case]", segmentPath);
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, _transcodingJobType);
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
}
@ -1325,11 +1354,10 @@ namespace Jellyfin.Api.Controllers
return result.ToArray();
}
private string GetCommandLineArguments(string outputPath, EncodingOptions encodingOptions, StreamState state, bool isEncoding, int startNumber)
private string GetCommandLineArguments(string outputPath, StreamState state, bool isEncoding, int startNumber)
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
var threads = _encodingHelper.GetNumberOfThreads(state, encodingOptions, videoCodec);
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
if (state.BaseRequest.BreakOnNonKeyFrames)
{
@ -1341,36 +1369,57 @@ namespace Jellyfin.Api.Controllers
state.BaseRequest.BreakOnNonKeyFrames = false;
}
var inputModifier = _encodingHelper.GetInputModifier(state, encodingOptions);
// If isEncoding is true we're actually starting ffmpeg
var startNumberParam = isEncoding ? startNumber.ToString(CultureInfo.InvariantCulture) : "0";
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
var outputExtension = GetSegmentFileExtension(state.Request.SegmentContainer);
var outputTsArg = outputPrefix + "%d" + outputExtension;
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + GetSegmentFileExtension(state.Request.SegmentContainer);
var segmentFormat = GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
var outputFmp4HeaderArg = string.Empty;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
// on Windows, the path of fmp4 header file needs to be configured
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
}
else
{
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
}
var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128
? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
else
{
_logger.LogError("Invalid HLS segment container: " + segmentFormat);
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
: "128";
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"",
inputModifier,
_encodingHelper.GetInputArgument(state, encodingOptions),
_encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
mapArgs,
GetVideoArguments(state, encodingOptions, startNumber),
GetAudioArguments(state, encodingOptions),
GetVideoArguments(state, startNumber),
GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
segmentFormat,
@ -1379,50 +1428,63 @@ namespace Jellyfin.Api.Controllers
outputPath).Trim();
}
private string GetAudioArguments(StreamState state, EncodingOptions encodingOptions)
/// <summary>
/// Gets the audio arguments for transcoding.
/// </summary>
/// <param name="state">The <see cref="StreamState"/>.</param>
/// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state)
{
if (state.AudioStream == null)
{
return string.Empty;
}
var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (EncodingHelper.IsCopyCodec(audioCodec))
{
return "-acodec copy";
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
return "-acodec copy -strict -2" + bitStreamArgs;
}
var audioTranscodeParams = new List<string>();
var audioTranscodeParams = string.Empty;
audioTranscodeParams.Add("-acodec " + audioCodec);
audioTranscodeParams += "-acodec " + audioCodec;
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams.Add("-ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture));
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams.Add("-ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture));
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams.Add("-ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture));
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
audioTranscodeParams.Add("-vn");
return string.Join(' ', audioTranscodeParams);
audioTranscodeParams += " -vn";
return audioTranscodeParams;
}
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
{
return "-codec:a:0 copy -copypriorss:a:0 0";
return "-codec:a:0 copy -strict -2 -copypriorss:a:0 0" + bitStreamArgs;
}
return "-codec:a:0 copy";
return "-codec:a:0 copy -strict -2" + bitStreamArgs;
}
var args = "-codec:a:0 " + audioCodec;
@ -1446,94 +1508,89 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
args += " " + _encodingHelper.GetAudioFilterParam(state, encodingOptions, true);
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
private string GetVideoArguments(StreamState state, EncodingOptions encodingOptions, int startNumber)
/// <summary>
/// Gets the video arguments for transcoding.
/// </summary>
/// <param name="state">The <see cref="StreamState"/>.</param>
/// <param name="startNumber">The first number in the hls sequence.</param>
/// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state, int startNumber)
{
if (state.VideoStream == null)
{
return string.Empty;
}
if (!state.IsOutputVideo)
{
return string.Empty;
}
var codec = _encodingHelper.GetVideoEncoder(state, encodingOptions);
var codec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var args = "-codec:v:0 " + codec;
// Prefer hvc1 to hev1.
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
args += " -tag:v:0 hvc1";
}
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
// See if we can save come cpu cycles by avoiding encoding
// See if we can save come cpu cycles by avoiding encoding.
if (EncodingHelper.IsCopyCodec(codec))
{
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
args += " -start_at_zero";
// args += " -flags -global_header";
}
else
{
var gopArg = string.Empty;
var keyFrameArg = string.Format(
CultureInfo.InvariantCulture,
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
startNumber * state.SegmentLength,
state.SegmentLength);
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
var framerate = state.VideoStream?.RealFrameRate;
// Set the key frame params for video encoding to match the hls segment time.
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, false, startNumber);
if (framerate.HasValue)
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
// This is to make sure keyframe interval is limited to our segment,
// as forcing keyframes is not enough.
// Example: we encoded half of desired length, then codec detected
// scene cut and inserted a keyframe; next forced keyframe would
// be created outside of segment, which breaks seeking
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe
gopArg = string.Format(
CultureInfo.InvariantCulture,
" -g {0} -keyint_min {0} -sc_threshold 0",
Math.Ceiling(state.SegmentLength * framerate.Value));
}
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, encodingOptions, "veryfast");
// Unable to force key frames using these hw encoders, set key frames by GOP
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase))
{
args += " " + gopArg;
}
else
{
args += " " + keyFrameArg + gopArg;
args += " -bf 0";
}
// args += " -mixed-refs 0 -refs 3 -x264opts b_pyramid=0:weightb=0:weightp=0";
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
// This is for graphical subs
if (hasGraphicalSubs)
{
args += _encodingHelper.GetGraphicalSubtitleParam(state, encodingOptions, codec);
// Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
// Add resolution params, if specified
else
{
args += _encodingHelper.GetOutputSizeParam(state, encodingOptions, codec);
// Resolution params.
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
}
// -start_at_zero is necessary to use with -ss when seeking,
@ -1693,7 +1750,7 @@ namespace Jellyfin.Api.Controllers
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension)
{
var job = _transcodingJobHelper.GetTranscodingJob(playlist, _transcodingJobType);
var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType);
if (job == null || job.HasExited)
{

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -253,7 +253,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="userId">User id.</param>
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</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="enableImages">Optional. include image information in output.</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,
[FromQuery] Guid? parentId,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
[FromQuery] string? includeItemTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
[FromQuery] bool? isPlayed,
[FromQuery] bool? enableImages,
[FromQuery] int? imageTypeLimit,
@ -296,7 +296,7 @@ namespace Jellyfin.Api.Controllers
new LatestItemsQuery
{
GroupItems = groupItems,
IncludeItemTypes = RequestHelpers.Split(includeItemTypes, ',', true),
IncludeItemTypes = includeItemTypes,
IsPlayed = isPlayed,
Limit = limit,
ParentId = parentId ?? Guid.Empty,

View file

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

View file

@ -1,8 +1,9 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
@ -145,7 +146,7 @@ namespace Jellyfin.Api.Controllers
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
/// <param name="requireAvc">Optional. Whether to require avc.</param>
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
/// <param name="liveStreamId">The live stream id.</param>
@ -296,23 +297,23 @@ namespace Jellyfin.Api.Controllers
.ConfigureAwait(false);
TranscodingJobDto? job = null;
var playlist = state.OutputFilePath;
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
if (!System.IO.File.Exists(playlist))
if (!System.IO.File.Exists(playlistPath))
{
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlist);
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try
{
if (!System.IO.File.Exists(playlist))
if (!System.IO.File.Exists(playlistPath))
{
// If the playlist doesn't already exist, startup ffmpeg
try
{
job = await _transcodingJobHelper.StartFfMpeg(
state,
playlist,
GetCommandLineArguments(playlist, state),
playlistPath,
GetCommandLineArguments(playlistPath, state),
Request,
TranscodingJobType,
cancellationTokenSource)
@ -328,7 +329,7 @@ namespace Jellyfin.Api.Controllers
minSegments = state.MinSegments;
if (minSegments > 0)
{
await HlsHelpers.WaitForMinimumSegmentCount(playlist, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
await HlsHelpers.WaitForMinimumSegmentCount(playlistPath, minSegments, _logger, cancellationTokenSource.Token).ConfigureAwait(false);
}
}
}
@ -338,14 +339,14 @@ namespace Jellyfin.Api.Controllers
}
}
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlist, TranscodingJobType);
job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job != null)
{
_transcodingJobHelper.OnTranscodeEndRequest(job);
}
var playlistText = HlsHelpers.GetLivePlaylistText(playlist, state.SegmentLength);
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
return Content(playlistText, MimeTypes.GetMimeType("playlist.m3u8"));
}
@ -359,17 +360,46 @@ namespace Jellyfin.Api.Controllers
private string GetCommandLineArguments(string outputPath, StreamState state)
{
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
var threads = _encodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec);
var threads = EncodingHelper.GetNumberOfThreads(state, _encodingOptions, videoCodec); // GetNumberOfThreads is static.
var inputModifier = _encodingHelper.GetInputModifier(state, _encodingOptions);
var format = !string.IsNullOrWhiteSpace(state.Request.SegmentContainer) ? "." + state.Request.SegmentContainer : ".ts";
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputTsArg = Path.Combine(directory, Path.GetFileNameWithoutExtension(outputPath)) + "%d" + format;
var mapArgs = state.IsOutputVideo ? _encodingHelper.GetMapArgs(state) : string.Empty;
var segmentFormat = format.TrimStart('.');
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
var outputTsArg = outputPrefix + "%d" + outputExtension;
var segmentFormat = outputExtension.TrimStart('.');
if (string.Equals(segmentFormat, "ts", StringComparison.OrdinalIgnoreCase))
{
segmentFormat = "mpegts";
}
else if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
var outputFmp4HeaderArg = string.Empty;
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
// on Windows, the path of fmp4 header file needs to be configured
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputPrefix + "-1" + outputExtension + "\"";
}
else
{
// on Linux/Unix, ffmpeg generate fmp4 header file to m3u8 output folder
outputFmp4HeaderArg = " -hls_fmp4_init_filename \"" + outputFileNameWithoutExtension + "-1" + outputExtension + "\"";
}
segmentFormat = "fmp4" + outputFmp4HeaderArg;
}
else
{
_logger.LogError("Invalid HLS segment container: {SegmentFormat}", segmentFormat);
}
var maxMuxingQueueSize = _encodingOptions.MaxMuxingQueueSize > 128
? _encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture)
: "128";
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
@ -378,20 +408,19 @@ namespace Jellyfin.Api.Controllers
return string.Format(
CultureInfo.InvariantCulture,
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -f segment -max_delay 5000000 -avoid_negative_ts disabled -start_at_zero -segment_time {6} {7} -individual_header_trailer 0 -segment_format {8} -segment_list_entry_prefix {9} -segment_list_type m3u8 -segment_start_number 0 -segment_list \"{10}\" -y \"{11}\"",
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -hls_segment_type {8} -start_number 0 -hls_base_url {9} -hls_playlist_type event -hls_segment_filename \"{10}\" -y \"{11}\"",
inputModifier,
_encodingHelper.GetInputArgument(state, _encodingOptions),
threads,
_encodingHelper.GetMapArgs(state),
mapArgs,
GetVideoArguments(state),
GetAudioArguments(state),
maxMuxingQueueSize,
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
string.Empty,
segmentFormat,
baseUrlParam,
outputPath,
outputTsArg)
.Trim();
outputTsArg,
outputPath).Trim();
}
/// <summary>
@ -401,14 +430,53 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for audio transcoding.</returns>
private string GetAudioArguments(StreamState state)
{
var codec = _encodingHelper.GetAudioEncoder(state);
if (EncodingHelper.IsCopyCodec(codec))
if (state.AudioStream == null)
{
return "-codec:a:0 copy";
return string.Empty;
}
var args = "-codec:a:0 " + codec;
var audioCodec = _encodingHelper.GetAudioEncoder(state);
if (!state.IsOutputVideo)
{
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
return "-acodec copy -strict -2" + bitStreamArgs;
}
var audioTranscodeParams = string.Empty;
audioTranscodeParams += "-acodec " + audioCodec;
if (state.OutputAudioBitrate.HasValue)
{
audioTranscodeParams += " -ab " + state.OutputAudioBitrate.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioChannels.HasValue)
{
audioTranscodeParams += " -ac " + state.OutputAudioChannels.Value.ToString(CultureInfo.InvariantCulture);
}
if (state.OutputAudioSampleRate.HasValue)
{
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
audioTranscodeParams += " -vn";
return audioTranscodeParams;
}
if (EncodingHelper.IsCopyCodec(audioCodec))
{
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
return "-acodec copy -strict -2" + bitStreamArgs;
}
var args = "-codec:a:0 " + audioCodec;
var channels = state.OutputAudioChannels;
@ -429,7 +497,7 @@ namespace Jellyfin.Api.Controllers
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
}
args += " " + _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
args += _encodingHelper.GetAudioFilterParam(state, _encodingOptions, true);
return args;
}
@ -441,6 +509,11 @@ namespace Jellyfin.Api.Controllers
/// <returns>The command line arguments for video transcoding.</returns>
private string GetVideoArguments(StreamState state)
{
if (state.VideoStream == null)
{
return string.Empty;
}
if (!state.IsOutputVideo)
{
return string.Empty;
@ -450,46 +523,64 @@ namespace Jellyfin.Api.Controllers
var args = "-codec:v:0 " + codec;
// Prefer hvc1 to hev1.
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
args += " -tag:v:0 hvc1";
}
// if (state.EnableMpegtsM2TsMode)
// {
// args += " -mpegts_m2ts_mode 1";
// }
// See if we can save come cpu cycles by avoiding encoding
if (codec.Equals("copy", StringComparison.OrdinalIgnoreCase))
// See if we can save come cpu cycles by avoiding encoding.
if (EncodingHelper.IsCopyCodec(codec))
{
// if h264_mp4toannexb is ever added, do not use it for live tv
if (state.VideoStream != null &&
!string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
// If h264_mp4toannexb is ever added, do not use it for live tv.
if (state.VideoStream != null && !string.Equals(state.VideoStream.NalLengthSize, "0", StringComparison.OrdinalIgnoreCase))
{
string bitStreamArgs = _encodingHelper.GetBitStreamArgs(state.VideoStream);
string bitStreamArgs = EncodingHelper.GetBitStreamArgs(state.VideoStream);
if (!string.IsNullOrEmpty(bitStreamArgs))
{
args += " " + bitStreamArgs;
}
}
args += " -start_at_zero";
}
else
{
var keyFrameArg = string.Format(
CultureInfo.InvariantCulture,
" -force_key_frames \"expr:gte(t,n_forced*{0})\"",
state.SegmentLength.ToString(CultureInfo.InvariantCulture));
args += _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset);
// Set the key frame params for video encoding to match the hls segment time.
args += _encodingHelper.GetHlsVideoKeyFrameArguments(state, codec, state.SegmentLength, true, null);
// Currenly b-frames in libx265 breaks the FMP4-HLS playback on iOS, disable it for now.
if (string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase))
{
args += " -bf 0";
}
var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
args += " " + _encodingHelper.GetVideoQualityParam(state, codec, _encodingOptions, DefaultEncoderPreset) + keyFrameArg;
// Add resolution params, if specified
if (!hasGraphicalSubs)
if (hasGraphicalSubs)
{
// Graphical subs overlay and resolution params.
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
}
else
{
// Resolution params.
args += _encodingHelper.GetOutputSizeParam(state, _encodingOptions, codec);
}
// This is for internal graphical subs
if (hasGraphicalSubs)
if (state.SubtitleStream == null || !state.SubtitleStream.IsExternal || state.SubtitleStream.IsTextSubtitleStream)
{
args += _encodingHelper.GetGraphicalSubtitleParam(state, _encodingOptions, codec);
args += " -start_at_zero";
}
}

View file

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

View file

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

View file

@ -207,7 +207,61 @@ namespace Jellyfin.Api.Helpers
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream != null && state.VideoRequest != null)
{
// Provide SDR HEVC entrance for backward compatibility.
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
&& string.Equals(state.VideoStream.VideoRange, "HDR", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
if (requestedVideoProfiles != null && requestedVideoProfiles.Length > 0)
{
// Force HEVC Main Profile and disable video stream copy.
state.OutputVideoCodec = "hevc";
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(",", requestedVideoProfiles), "main");
sdrVideoUrl += "&AllowVideoStreamCopy=false";
EncodingHelper encodingHelper = new EncodingHelper(_mediaEncoder, _fileSystem, _subtitleEncoder, _configuration);
var sdrOutputVideoBitrate = encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec) ?? 0;
var sdrOutputAudioBitrate = encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream) ?? 0;
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Restore the video codec
state.OutputVideoCodec = "copy";
}
}
// Provide Level 5.0 entrance for backward compatibility.
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
// but in fact it is capable of playing videos up to Level 6.1.
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150
&& !string.IsNullOrEmpty(state.VideoStream.VideoRange)
&& string.Equals(state.VideoStream.VideoRange, "SDR", StringComparison.OrdinalIgnoreCase)
&& string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
var playlistCodecsField = new StringBuilder();
AppendPlaylistCodecsField(playlistCodecsField, state);
// Force the video level to 5.0.
var originalLevel = state.VideoStream.Level;
state.VideoStream.Level = 150;
var newPlaylistCodecsField = new StringBuilder();
AppendPlaylistCodecsField(newPlaylistCodecsField, state);
// Restore the video level.
state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist);
}
}
if (EnableAdaptiveBitrateStreaming(state, isLiveStream, enableAdaptiveBitrateStreaming, _httpContextAccessor.HttpContext.GetNormalizedRemoteIp()))
{
@ -217,40 +271,77 @@ namespace Jellyfin.Api.Helpers
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
var variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
variantUrl = ReplaceBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
private void AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
private StringBuilder AppendPlaylist(StringBuilder builder, StreamState state, string url, int bitrate, string? subtitleGroup)
{
builder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
var playlistBuilder = new StringBuilder();
playlistBuilder.Append("#EXT-X-STREAM-INF:BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture))
.Append(",AVERAGE-BANDWIDTH=")
.Append(bitrate.ToString(CultureInfo.InvariantCulture));
AppendPlaylistCodecsField(builder, state);
AppendPlaylistVideoRangeField(playlistBuilder, state);
AppendPlaylistResolutionField(builder, state);
AppendPlaylistCodecsField(playlistBuilder, state);
AppendPlaylistFramerateField(builder, state);
AppendPlaylistResolutionField(playlistBuilder, state);
AppendPlaylistFramerateField(playlistBuilder, state);
if (!string.IsNullOrWhiteSpace(subtitleGroup))
{
builder.Append(",SUBTITLES=\"")
playlistBuilder.Append(",SUBTITLES=\"")
.Append(subtitleGroup)
.Append('"');
}
builder.Append(Environment.NewLine);
builder.AppendLine(url);
playlistBuilder.Append(Environment.NewLine);
playlistBuilder.AppendLine(url);
builder.Append(playlistBuilder);
return playlistBuilder;
}
/// <summary>
/// Appends a VIDEO-RANGE field containing the range of the output video stream.
/// </summary>
/// <seealso cref="AppendPlaylist(StringBuilder, StreamState, string, int, string)"/>
/// <param name="builder">StringBuilder to append the field to.</param>
/// <param name="state">StreamState of the current stream.</param>
private void AppendPlaylistVideoRangeField(StringBuilder builder, StreamState state)
{
if (state.VideoStream != null && !string.IsNullOrEmpty(state.VideoStream.VideoRange))
{
var videoRange = state.VideoStream.VideoRange;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
if (string.Equals(videoRange, "SDR", StringComparison.OrdinalIgnoreCase))
{
builder.Append(",VIDEO-RANGE=SDR");
}
if (string.Equals(videoRange, "HDR", StringComparison.OrdinalIgnoreCase))
{
builder.Append(",VIDEO-RANGE=PQ");
}
}
else
{
// Currently we only encode to SDR.
builder.Append(",VIDEO-RANGE=SDR");
}
}
}
/// <summary>
@ -419,15 +510,27 @@ namespace Jellyfin.Api.Helpers
/// <returns>H.26X level of the output video stream.</returns>
private int? GetOutputVideoCodecLevel(StreamState state)
{
string? levelString;
string levelString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream != null
&& state.VideoStream.Level.HasValue)
{
levelString = state.VideoStream?.Level.ToString();
levelString = state.VideoStream.Level.ToString() ?? string.Empty;
}
else
{
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec);
if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
levelString = state.GetRequestedLevel(state.ActualOutputVideoCodec) ?? "41";
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
levelString = state.GetRequestedLevel("h265") ?? state.GetRequestedLevel("hevc") ?? "120";
levelString = EncodingHelper.NormalizeTranscodingLevel(state, levelString);
}
}
if (int.TryParse(levelString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedLevel))
@ -438,6 +541,38 @@ namespace Jellyfin.Api.Helpers
return null;
}
/// <summary>
/// Get the H.26X profile of the output video stream.
/// </summary>
/// <param name="state">StreamState of the current stream.</param>
/// <param name="codec">Video codec.</param>
/// <returns>H.26X profile of the output video stream.</returns>
private string GetOutputVideoCodecProfile(StreamState state, string codec)
{
string profileString = string.Empty;
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& !string.IsNullOrEmpty(state.VideoStream.Profile))
{
profileString = state.VideoStream.Profile;
}
else if (!string.IsNullOrEmpty(codec))
{
profileString = state.GetRequestedProfiles(codec).FirstOrDefault() ?? string.Empty;
if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
profileString = profileString ?? "high";
}
if (string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
profileString = profileString ?? "main";
}
}
return profileString;
}
/// <summary>
/// Gets a formatted string of the output audio codec, for use in the CODECS field.
/// </summary>
@ -468,6 +603,16 @@ namespace Jellyfin.Api.Helpers
return HlsCodecStringHelpers.GetEAC3String();
}
if (string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetFLACString();
}
if (string.Equals(state.ActualOutputAudioCodec, "alac", StringComparison.OrdinalIgnoreCase))
{
return HlsCodecStringHelpers.GetALACString();
}
return string.Empty;
}
@ -492,15 +637,14 @@ namespace Jellyfin.Api.Helpers
if (string.Equals(codec, "h264", StringComparison.OrdinalIgnoreCase))
{
string? profile = state.GetRequestedProfiles("h264").FirstOrDefault();
string profile = GetOutputVideoCodecProfile(state, "h264");
return HlsCodecStringHelpers.GetH264String(profile, level);
}
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase))
{
string? profile = state.GetRequestedProfiles("h265").FirstOrDefault();
string profile = GetOutputVideoCodecProfile(state, "hevc");
return HlsCodecStringHelpers.GetH265String(profile, level);
}
@ -544,12 +688,30 @@ namespace Jellyfin.Api.Helpers
return variation;
}
private string ReplaceBitrate(string url, int oldValue, int newValue)
private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
{
return url.Replace(
"videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
"videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase);
}
private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
{
string profileStr = codec + "-profile=";
return url.Replace(
profileStr + oldValue,
profileStr + newValue,
StringComparison.OrdinalIgnoreCase);
}
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
{
var oldPlaylist = playlist.ToString();
return oldPlaylist.Replace(
oldValue.ToString(),
newValue.ToString(),
StringComparison.OrdinalIgnoreCase);
}
}
}

View file

@ -9,13 +9,38 @@ namespace Jellyfin.Api.Helpers
/// </summary>
public static class HlsCodecStringHelpers
{
/// <summary>
/// Codec name for MP3.
/// </summary>
public const string MP3 = "mp4a.40.34";
/// <summary>
/// Codec name for AC-3.
/// </summary>
public const string AC3 = "mp4a.a5";
/// <summary>
/// Codec name for E-AC-3.
/// </summary>
public const string EAC3 = "mp4a.a6";
/// <summary>
/// Codec name for FLAC.
/// </summary>
public const string FLAC = "fLaC";
/// <summary>
/// Codec name for ALAC.
/// </summary>
public const string ALAC = "alac";
/// <summary>
/// Gets a MP3 codec string.
/// </summary>
/// <returns>MP3 codec string.</returns>
public static string GetMP3String()
{
return "mp4a.40.34";
return MP3;
}
/// <summary>
@ -40,6 +65,42 @@ namespace Jellyfin.Api.Helpers
return result.ToString();
}
/// <summary>
/// Gets an AC-3 codec string.
/// </summary>
/// <returns>AC-3 codec string.</returns>
public static string GetAC3String()
{
return AC3;
}
/// <summary>
/// Gets an E-AC-3 codec string.
/// </summary>
/// <returns>E-AC-3 codec string.</returns>
public static string GetEAC3String()
{
return EAC3;
}
/// <summary>
/// Gets an FLAC codec string.
/// </summary>
/// <returns>FLAC codec string.</returns>
public static string GetFLACString()
{
return FLAC;
}
/// <summary>
/// Gets an ALAC codec string.
/// </summary>
/// <returns>ALAC codec string.</returns>
public static string GetALACString()
{
return ALAC;
}
/// <summary>
/// Gets a H.264 codec string.
/// </summary>
@ -85,41 +146,24 @@ namespace Jellyfin.Api.Helpers
// The h265 syntax is a bit of a mystery at the time this comment was written.
// This is what I've found through various sources:
// FORMAT: [codecTag].[profile].[constraint?].L[level * 30].[UNKNOWN]
StringBuilder result = new StringBuilder("hev1", 16);
StringBuilder result = new StringBuilder("hvc1", 16);
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase))
if (string.Equals(profile, "main10", StringComparison.OrdinalIgnoreCase)
|| string.Equals(profile, "main 10", StringComparison.OrdinalIgnoreCase))
{
result.Append(".2.6");
result.Append(".2.4");
}
else
{
// Default to main if profile is invalid
result.Append(".1.6");
result.Append(".1.4");
}
result.Append(".L")
.Append(level * 3)
.Append(level)
.Append(".B0");
return result.ToString();
}
/// <summary>
/// Gets an AC-3 codec string.
/// </summary>
/// <returns>AC-3 codec string.</returns>
public static string GetAC3String()
{
return "mp4a.a5";
}
/// <summary>
/// Gets an E-AC-3 codec string.
/// </summary>
/// <returns>E-AC-3 codec string.</returns>
public static string GetEAC3String()
{
return "mp4a.a6";
}
}
}

View file

@ -1,8 +1,11 @@
using System;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
@ -74,25 +77,65 @@ namespace Jellyfin.Api.Helpers
}
}
/// <summary>
/// Gets the #EXT-X-MAP string.
/// </summary>
/// <param name="outputPath">The output path of the file.</param>
/// <param name="state">The <see cref="StreamState"/>.</param>
/// <param name="isOsDepends">Get a normal string or depends on OS.</param>
/// <returns>The string text of #EXT-X-MAP.</returns>
public static string GetFmp4InitFileName(string outputPath, StreamState state, bool isOsDepends)
{
var directory = Path.GetDirectoryName(outputPath) ?? throw new ArgumentException($"Provided path ({outputPath}) is not valid.", nameof(outputPath));
var outputFileNameWithoutExtension = Path.GetFileNameWithoutExtension(outputPath);
var outputPrefix = Path.Combine(directory, outputFileNameWithoutExtension);
var outputExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
// on Linux/Unix
// #EXT-X-MAP:URI="prefix-1.mp4"
var fmp4InitFileName = outputFileNameWithoutExtension + "-1" + outputExtension;
if (!isOsDepends)
{
return fmp4InitFileName;
}
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
// on Windows
// #EXT-X-MAP:URI="X:\transcodes\prefix-1.mp4"
fmp4InitFileName = outputPrefix + "-1" + outputExtension;
}
return fmp4InitFileName;
}
/// <summary>
/// Gets the hls playlist text.
/// </summary>
/// <param name="path">The path to the playlist file.</param>
/// <param name="segmentLength">The segment length.</param>
/// <param name="state">The <see cref="StreamState"/>.</param>
/// <returns>The playlist text as a string.</returns>
public static string GetLivePlaylistText(string path, int segmentLength)
public static string GetLivePlaylistText(string path, StreamState state)
{
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
text = text.Replace("#EXTM3U", "#EXTM3U\n#EXT-X-PLAYLIST-TYPE:EVENT", StringComparison.InvariantCulture);
var segmentFormat = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer).TrimStart('.');
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase))
{
var fmp4InitFileName = GetFmp4InitFileName(path, state, true);
var baseUrlParam = string.Format(
CultureInfo.InvariantCulture,
"hls/{0}/",
Path.GetFileNameWithoutExtension(path));
var newFmp4InitFileName = baseUrlParam + GetFmp4InitFileName(path, state, false);
var newDuration = "#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture);
text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength - 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
// text = text.Replace("#EXT-X-TARGETDURATION:" + (segmentLength + 1).ToString(CultureInfo.InvariantCulture), newDuration, StringComparison.OrdinalIgnoreCase);
// Replace fMP4 init file URI.
text = text.Replace(fmp4InitFileName, newFmp4InitFileName, StringComparison.InvariantCulture);
}
return text;
}

View file

@ -122,49 +122,6 @@ namespace Jellyfin.Api.Helpers
return session;
}
/// <summary>
/// Get Guid array from string.
/// </summary>
/// <param name="value">String value.</param>
/// <returns>Guid array.</returns>
internal static Guid[] GetGuids(string? value)
{
if (value == null)
{
return Array.Empty<Guid>();
}
return Split(value, ',', true)
.Select(i => new Guid(i))
.ToArray();
}
/// <summary>
/// Gets the item fields.
/// </summary>
/// <param name="fields">The fields string.</param>
/// <returns>IEnumerable{ItemFields}.</returns>
internal static ItemFields[] GetItemFields(string? fields)
{
if (string.IsNullOrEmpty(fields))
{
return Array.Empty<ItemFields>();
}
return Split(fields, ',', true)
.Select(v =>
{
if (Enum.TryParse(v, true, out ItemFields value))
{
return (ItemFields?)value;
}
return null;
}).Where(i => i.HasValue)
.Select(i => i!.Value)
.ToArray();
}
internal static QueryResult<BaseItemDto> CreateQueryResult(
QueryResult<(BaseItem, ItemCounts)> result,
DtoOptions dtoOptions,

View file

@ -169,7 +169,9 @@ namespace Jellyfin.Api.Helpers
state.DirectStreamProvider = liveStreamInfo.Item2;
}
encodingHelper.AttachMediaSourceInfo(state, mediaSource, url);
var encodingOptions = serverConfigurationManager.GetEncodingOptions();
encodingHelper.AttachMediaSourceInfo(state, encodingOptions, mediaSource, url);
string? containerInternal = Path.GetExtension(state.RequestedUrl);
@ -187,7 +189,7 @@ namespace Jellyfin.Api.Helpers
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, state.AudioStream);
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream);
state.OutputAudioCodec = streamingRequest.AudioCodec;
@ -200,20 +202,41 @@ namespace Jellyfin.Api.Helpers
encodingHelper.TryStreamCopy(state);
if (state.OutputVideoBitrate.HasValue && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
if (!EncodingHelper.IsCopyCodec(state.OutputVideoCodec) && state.OutputVideoBitrate.HasValue)
{
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream?.BitRate,
state.VideoStream?.Width,
state.VideoStream?.Height,
state.OutputVideoBitrate.Value,
state.VideoStream?.Codec,
state.OutputVideoCodec,
state.VideoRequest.MaxWidth,
state.VideoRequest.MaxHeight);
var isVideoResolutionNotRequested = !state.VideoRequest.Width.HasValue
&& !state.VideoRequest.Height.HasValue
&& !state.VideoRequest.MaxWidth.HasValue
&& !state.VideoRequest.MaxHeight.HasValue;
state.VideoRequest.MaxWidth = resolution.MaxWidth;
state.VideoRequest.MaxHeight = resolution.MaxHeight;
if (isVideoResolutionNotRequested
&& state.VideoRequest.VideoBitRate.HasValue
&& state.VideoStream.BitRate.HasValue
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
{
// Don't downscale the resolution if the width/height/MaxWidth/MaxHeight is not requested,
// and the requested video bitrate is higher than source video bitrate.
if (state.VideoStream.Width.HasValue || state.VideoStream.Height.HasValue)
{
state.VideoRequest.MaxWidth = state.VideoStream?.Width;
state.VideoRequest.MaxHeight = state.VideoStream?.Height;
}
}
else
{
var resolution = ResolutionNormalizer.Normalize(
state.VideoStream?.BitRate,
state.VideoStream?.Width,
state.VideoStream?.Height,
state.OutputVideoBitrate.Value,
state.VideoStream?.Codec,
state.OutputVideoCodec,
state.VideoRequest.MaxWidth,
state.VideoRequest.MaxHeight);
state.VideoRequest.MaxWidth = resolution.MaxWidth;
state.VideoRequest.MaxHeight = resolution.MaxHeight;
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
@ -145,7 +145,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
// Progressive streams can stop on their own reliably
// Progressive streams can stop on their own reliably.
jobs = _activeTranscodingJobs.Where(j => string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase)).ToList();
}
@ -241,7 +241,7 @@ namespace Jellyfin.Api.Helpers
lock (_activeTranscodingJobs)
{
// This is really only needed for HLS.
// Progressive streams can stop on their own reliably
// Progressive streams can stop on their own reliably.
jobs.AddRange(_activeTranscodingJobs.Where(killJob));
}
@ -304,10 +304,10 @@ namespace Jellyfin.Api.Helpers
process!.StandardInput.WriteLine("q");
// Need to wait because killing is asynchronous
// Need to wait because killing is asynchronous.
if (!process.WaitForExit(5000))
{
_logger.LogInformation("Killing ffmpeg process for {Path}", job.Path);
_logger.LogInformation("Killing FFmpeg process for {Path}", job.Path);
process.Kill();
}
}
@ -470,11 +470,11 @@ namespace Jellyfin.Api.Helpers
}
/// <summary>
/// Starts the FFMPEG.
/// Starts FFmpeg.
/// </summary>
/// <param name="state">The state.</param>
/// <param name="outputPath">The output path.</param>
/// <param name="commandLineArguments">The command line arguments for ffmpeg.</param>
/// <param name="commandLineArguments">The command line arguments for FFmpeg.</param>
/// <param name="request">The <see cref="HttpRequest"/>.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The cancellation token source.</param>
@ -501,13 +501,13 @@ namespace Jellyfin.Api.Helpers
{
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw new ArgumentException("User does not have access to video transcoding");
throw new ArgumentException("User does not have access to video transcoding.");
}
}
if (string.IsNullOrEmpty(_mediaEncoder.EncoderPath))
{
throw new ArgumentException("FFMPEG path not set.");
throw new ArgumentException("FFmpeg path not set.");
}
var process = new Process
@ -544,18 +544,20 @@ namespace Jellyfin.Api.Helpers
var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments;
_logger.LogInformation(commandLineLogMessage);
var logFilePrefix = "ffmpeg-transcode";
var logFilePrefix = "FFmpeg.Transcode-";
if (state.VideoRequest != null
&& EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
logFilePrefix = EncodingHelper.IsCopyCodec(state.OutputAudioCodec)
? "ffmpeg-remux"
: "ffmpeg-directstream";
? "FFmpeg.Remux-"
: "FFmpeg.DirectStream-";
}
var logFilePath = Path.Combine(_serverConfigurationManager.ApplicationPaths.LogDirectoryPath, logFilePrefix + "-" + Guid.NewGuid() + ".txt");
var logFilePath = Path.Combine(
_serverConfigurationManager.ApplicationPaths.LogDirectoryPath,
$"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log");
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
// FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
@ -569,20 +571,20 @@ namespace Jellyfin.Api.Helpers
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
_logger.LogError(ex, "Error starting FFmpeg");
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
throw;
}
_logger.LogDebug("Launched ffmpeg process");
_logger.LogDebug("Launched FFmpeg process");
state.TranscodingJob = transcodingJob;
// Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
// Wait for the file to exist before proceeeding
// Wait for the file to exist before proceeding
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
_logger.LogDebug("Waiting for the creation of {0}", ffmpegTargetFile);
while (!File.Exists(ffmpegTargetFile) && !transcodingJob.HasExited)
@ -748,11 +750,11 @@ namespace Jellyfin.Api.Helpers
if (process.ExitCode == 0)
{
_logger.LogInformation("FFMpeg exited with code 0");
_logger.LogInformation("FFmpeg exited with code 0");
}
else
{
_logger.LogError("FFMpeg exited with code {0}", process.ExitCode);
_logger.LogError("FFmpeg exited with code {0}", process.ExitCode);
}
process.Dispose();
@ -771,8 +773,9 @@ namespace Jellyfin.Api.Helpers
new LiveStreamRequest { OpenToken = state.MediaSource.OpenToken },
cancellationTokenSource.Token)
.ConfigureAwait(false);
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
_encodingHelper.AttachMediaSourceInfo(state, liveStreamResponse.MediaSource, state.RequestedUrl);
_encodingHelper.AttachMediaSourceInfo(state, encodingOptions, liveStreamResponse.MediaSource, state.RequestedUrl);
if (state.VideoRequest != null)
{

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

View file

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

View file

@ -73,7 +73,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the next item in the collection.
/// </summary>
/// <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>
public virtual CollectionItem Next { get; set; }
@ -81,7 +81,7 @@ namespace Jellyfin.Data.Entities.Libraries
/// Gets or sets the previous item in the collection.
/// </summary>
/// <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>
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; }
/// <summary>
/// Gets or sets a collection containing the generes for this item.
/// Gets or sets a collection containing the genres for this item.
/// </summary>
public virtual ICollection<Genre> Genres { get; protected set; }

View file

@ -53,7 +53,7 @@ namespace Jellyfin.Server.Implementations.Users
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))
{
return Task.FromResult(new ProviderAuthenticationResult

View file

@ -43,7 +43,8 @@ namespace MediaBrowser.Common.Json.Converters
}
catch (FormatException)
{
// TODO log when upgraded to .Net5
// TODO log when upgraded to .Net6
// https://github.com/dotnet/runtime/issues/42975
// _logger.LogWarning(e, "Error converting value.");
}
}

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 JsonStringEnumConverter());
options.Converters.Add(new JsonNullableStructConverterFactory());
options.Converters.Add(new JsonDateTimeIso8601Converter());
return options;
}

View file

@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" />
</ItemGroup>

View file

@ -247,7 +247,23 @@ namespace MediaBrowser.Common.Plugins
}
catch
{
return (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
var config = (TConfigurationType)Activator.CreateInstance(typeof(TConfigurationType));
SaveConfiguration(config);
return config;
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
/// <param name="config">Configuration to save.</param>
public virtual void SaveConfiguration(TConfigurationType config)
{
lock (_configurationSaveLock)
{
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
}
}
@ -256,12 +272,7 @@ namespace MediaBrowser.Common.Plugins
/// </summary>
public virtual void SaveConfiguration()
{
lock (_configurationSaveLock)
{
_directoryCreateFn(Path.GetDirectoryName(ConfigurationFilePath));
XmlSerializer.SerializeToFile(Configuration, ConfigurationFilePath);
}
SaveConfiguration(Configuration);
}
/// <inheritdoc />
@ -274,7 +285,7 @@ namespace MediaBrowser.Common.Plugins
Configuration = (TConfigurationType)configuration;
SaveConfiguration();
SaveConfiguration(Configuration);
ConfigurationChanged?.Invoke(this, configuration);
}

View file

@ -2557,7 +2557,7 @@ namespace MediaBrowser.Controller.Entities
{
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);

View file

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

View file

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

View file

@ -791,7 +791,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
if (query.Genres.Length > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
if (query.Genres.Count > 0 && !query.Genres.Any(v => item.Genres.Contains(v, StringComparer.OrdinalIgnoreCase)))
{
return false;
}
@ -822,7 +822,7 @@ namespace MediaBrowser.Controller.Entities
}
// Apply genre filter
if (query.GenreIds.Length > 0 && !query.GenreIds.Any(id =>
if (query.GenreIds.Count > 0 && !query.GenreIds.Any(id =>
{
var genreItem = libraryManager.GetItemById(id);
return genreItem != null && item.Genres.Contains(genreItem.Name, StringComparer.OrdinalIgnoreCase);

View file

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
@ -23,7 +24,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
public class EncodingHelper
{
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
private static readonly CultureInfo _usCulture = new CultureInfo("en-US");
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
@ -63,7 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
// 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
// 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)
{
var hwType = encodingOptions.HardwareAccelerationType;
@ -247,7 +248,7 @@ namespace MediaBrowser.Controller.MediaEncoding
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))
{
return null;
@ -440,6 +441,12 @@ namespace MediaBrowser.Controller.MediaEncoding
return "libopus";
}
if (string.Equals(codec, "flac", StringComparison.OrdinalIgnoreCase))
{
// flac is experimental in mp4 muxer
return "flac -strict -2";
}
return codec.ToLowerInvariant();
}
@ -573,7 +580,7 @@ namespace MediaBrowser.Controller.MediaEncoding
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns><c>true</c> if the specified stream is H264; otherwise, <c>false</c>.</returns>
public bool IsH264(MediaStream stream)
public static bool IsH264(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@ -581,7 +588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("avc", StringComparison.OrdinalIgnoreCase) != -1;
}
public bool IsH265(MediaStream stream)
public static bool IsH265(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
@ -589,10 +596,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|| codec.IndexOf("hevc", StringComparison.OrdinalIgnoreCase) != -1;
}
// TODO This is auto inserted into the mpegts mux so it might not be needed
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
public string GetBitStreamArgs(MediaStream stream)
public static bool IsAAC(MediaStream stream)
{
var codec = stream.Codec ?? string.Empty;
return codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1;
}
public static string GetBitStreamArgs(MediaStream stream)
{
// TODO This is auto inserted into the mpegts mux so it might not be needed.
// https://www.ffmpeg.org/ffmpeg-bitstream-filters.html#h264_005fmp4toannexb
if (IsH264(stream))
{
return "-bsf:v h264_mp4toannexb";
@ -601,12 +615,44 @@ namespace MediaBrowser.Controller.MediaEncoding
{
return "-bsf:v hevc_mp4toannexb";
}
else if (IsAAC(stream))
{
// Convert adts header(mpegts) to asc header(mp4).
return "-bsf:a aac_adtstoasc";
}
else
{
return null;
}
}
public static string GetAudioBitStreamArguments(EncodingJobInfo state, string segmentContainer, string mediaSourceContainer)
{
var bitStreamArgs = string.Empty;
var segmentFormat = GetSegmentFileExtension(segmentContainer).TrimStart('.');
// Apply aac_adtstoasc bitstream filter when media source is in mpegts.
if (string.Equals(segmentFormat, "mp4", StringComparison.OrdinalIgnoreCase)
&& (string.Equals(mediaSourceContainer, "mpegts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(mediaSourceContainer, "hls", StringComparison.OrdinalIgnoreCase)))
{
bitStreamArgs = GetBitStreamArgs(state.AudioStream);
bitStreamArgs = string.IsNullOrEmpty(bitStreamArgs) ? string.Empty : " " + bitStreamArgs;
}
return bitStreamArgs;
}
public static string GetSegmentFileExtension(string segmentContainer)
{
if (!string.IsNullOrWhiteSpace(segmentContainer))
{
return "." + segmentContainer;
}
return ".ts";
}
public string GetVideoBitrateParam(EncodingJobInfo state, string videoCodec)
{
var bitrate = state.OutputVideoBitrate;
@ -654,16 +700,30 @@ namespace MediaBrowser.Controller.MediaEncoding
return string.Empty;
}
public string NormalizeTranscodingLevel(string videoCodec, string level)
public static string NormalizeTranscodingLevel(EncodingJobInfo state, string level)
{
// Clients may direct play higher than level 41, but there's no reason to transcode higher
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel)
&& requestLevel > 41
&& (string.Equals(videoCodec, "h264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)))
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double requestLevel))
{
return "41";
if (string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(state.ActualOutputVideoCodec, "h265", StringComparison.OrdinalIgnoreCase))
{
// Transcode to level 5.0 and lower for maximum compatibility.
// Level 5.0 is suitable for up to 4k 30fps hevc encoding, otherwise let the encoder to handle it.
// https://en.wikipedia.org/wiki/High_Efficiency_Video_Coding_tiers_and_levels
// MaxLumaSampleRate = 3840*2160*30 = 248832000 < 267386880.
if (requestLevel >= 150)
{
return "150";
}
}
else if (string.Equals(state.ActualOutputVideoCodec, "h264", StringComparison.OrdinalIgnoreCase))
{
// Clients may direct play higher than level 41, but there's no reason to transcode higher.
if (requestLevel >= 41)
{
return "41";
}
}
}
return level;
@ -766,6 +826,72 @@ namespace MediaBrowser.Controller.MediaEncoding
return null;
}
public string GetHlsVideoKeyFrameArguments(
EncodingJobInfo state,
string codec,
int segmentLength,
bool isEventPlaylist,
int? startNumber)
{
var args = string.Empty;
var gopArg = string.Empty;
var keyFrameArg = string.Empty;
if (isEventPlaylist)
{
keyFrameArg = string.Format(
CultureInfo.InvariantCulture,
" -force_key_frames:0 \"expr:gte(t,n_forced*{0})\"",
segmentLength);
}
else if (startNumber.HasValue)
{
keyFrameArg = string.Format(
CultureInfo.InvariantCulture,
" -force_key_frames:0 \"expr:gte(t,{0}+n_forced*{1})\"",
startNumber.Value * segmentLength,
segmentLength);
}
var framerate = state.VideoStream?.RealFrameRate;
if (framerate.HasValue)
{
// This is to make sure keyframe interval is limited to our segment,
// as forcing keyframes is not enough.
// Example: we encoded half of desired length, then codec detected
// scene cut and inserted a keyframe; next forced keyframe would
// be created outside of segment, which breaks seeking.
// -sc_threshold 0 is used to prevent the hardware encoder from post processing to break the set keyframe.
gopArg = string.Format(
CultureInfo.InvariantCulture,
" -g:v:0 {0} -keyint_min:v:0 {0} -sc_threshold:v:0 0",
Math.Ceiling(segmentLength * framerate.Value));
}
// Unable to force key frames using these encoders, set key frames by GOP.
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
args += gopArg;
}
else if (string.Equals(codec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "libx265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{
args += " " + keyFrameArg;
}
else
{
args += " " + keyFrameArg + gopArg;
}
return args;
}
/// <summary>
/// Gets the video bitrate to specify on the command line.
/// </summary>
@ -773,6 +899,47 @@ namespace MediaBrowser.Controller.MediaEncoding
{
var param = string.Empty;
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
param += " -pix_fmt yuv420p";
}
if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
var videoStream = state.VideoStream;
var isColorDepth10 = IsColorDepth10(state);
if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(videoStream.VideoRange)
&& videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
{
param += " -pix_fmt nv12";
}
else
{
param += " -pix_fmt yuv420p";
}
}
if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
param += " -pix_fmt nv21";
}
var isVc1 = state.VideoStream != null &&
string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
@ -781,11 +948,11 @@ namespace MediaBrowser.Controller.MediaEncoding
{
if (!string.IsNullOrEmpty(encodingOptions.EncoderPreset))
{
param += "-preset " + encodingOptions.EncoderPreset;
param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
param += "-preset " + defaultPreset;
param += " -preset " + defaultPreset;
}
int encodeCrf = encodingOptions.H264Crf;
@ -809,38 +976,40 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -crf " + defaultCrf;
}
}
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)) // h264 (h264_qsv)
else if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase) // h264 (h264_qsv)
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_qsv)
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
{
param += "-preset " + encodingOptions.EncoderPreset;
param += " -preset " + encodingOptions.EncoderPreset;
}
else
{
param += "-preset 7";
param += " -preset 7";
}
param += " -look_ahead 0";
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase) // h264 (h264_nvenc)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_nvenc)
{
// following preset will be deprecated in ffmpeg 4.4, use p1~p7 instead.
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
param += "-preset slow"; // lossless is only supported on maxwell and newer(2014+)
param += " -preset slow"; // lossless is only supported on maxwell and newer(2014+)
break;
case "slow":
case "slower":
param += "-preset slow";
param += " -preset slow";
break;
case "medium":
param += "-preset medium";
param += " -preset medium";
break;
case "fast":
@ -848,27 +1017,27 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
param += "-preset fast";
param += " -preset fast";
break;
default:
param += "-preset default";
param += " -preset default";
break;
}
}
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase) // h264 (h264_amf)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)) // hevc (hevc_amf)
{
switch (encodingOptions.EncoderPreset)
{
case "veryslow":
case "slow":
case "slower":
param += "-quality quality";
param += " -quality quality";
break;
case "medium":
param += "-quality balanced";
param += " -quality balanced";
break;
case "fast":
@ -876,11 +1045,11 @@ namespace MediaBrowser.Controller.MediaEncoding
case "veryfast":
case "superfast":
case "ultrafast":
param += "-quality speed";
param += " -quality speed";
break;
default:
param += "-quality speed";
param += " -quality speed";
break;
}
@ -896,6 +1065,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// Enhance workload when tone mapping with AMF on some APUs
param += " -preanalysis true";
}
if (string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
param += " -header_insertion_mode gop -gops_per_idr 1";
}
}
else if (string.Equals(videoEncoder, "libvpx", StringComparison.OrdinalIgnoreCase)) // webm
{
@ -917,7 +1091,7 @@ namespace MediaBrowser.Controller.MediaEncoding
profileScore = Math.Min(profileScore, 2);
// http://www.webmproject.org/docs/encoder-parameters/
param += string.Format(CultureInfo.InvariantCulture, "-speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
param += string.Format(CultureInfo.InvariantCulture, " -speed 16 -quality good -profile:v {0} -slices 8 -crf {1} -qmin {2} -qmax {3}",
profileScore.ToString(_usCulture),
crf,
qmin,
@ -925,15 +1099,15 @@ namespace MediaBrowser.Controller.MediaEncoding
}
else if (string.Equals(videoEncoder, "mpeg4", StringComparison.OrdinalIgnoreCase))
{
param += "-mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
param += " -mbd rd -flags +mv4+aic -trellis 2 -cmp 2 -subcmp 2 -bf 2";
}
else if (string.Equals(videoEncoder, "wmv2", StringComparison.OrdinalIgnoreCase)) // asf/wmv
{
param += "-qmin 2";
param += " -qmin 2";
}
else if (string.Equals(videoEncoder, "msmpeg4", StringComparison.OrdinalIgnoreCase))
{
param += "-mbd 2";
param += " -mbd 2";
}
param += GetVideoBitrateParam(state, videoEncoder);
@ -945,11 +1119,25 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var targetVideoCodec = state.ActualOutputVideoCodec;
if (string.Equals(targetVideoCodec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(targetVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
{
targetVideoCodec = "hevc";
}
var profile = state.GetRequestedProfiles(targetVideoCodec).FirstOrDefault();
profile = Regex.Replace(profile, @"\s+", String.Empty);
// vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly)
// Only libx264 support encoding H264 High 10 Profile, otherwise force High Profile.
if (!string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
&& profile != null
&& profile.IndexOf("high 10", StringComparison.OrdinalIgnoreCase) != -1)
{
profile = "high";
}
// h264_vaapi does not support Baseline profile, force Constrained Baseline in this case,
// which is compatible (and ugly).
if (string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& profile != null
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
@ -957,13 +1145,31 @@ namespace MediaBrowser.Controller.MediaEncoding
profile = "constrained_baseline";
}
// libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
&& profile != null
&& profile.IndexOf("baseline", StringComparison.OrdinalIgnoreCase) != -1)
{
profile = "baseline";
}
// Currently hevc_amf only support encoding HEVC Main Profile, otherwise force Main Profile.
if (!string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase)
&& profile != null
&& profile.IndexOf("main 10", StringComparison.OrdinalIgnoreCase) != -1)
{
profile = "main";
}
if (!string.IsNullOrEmpty(profile))
{
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
// not supported by h264_omx
param += " -profile:v " + profile;
param += " -profile:v:0 " + profile;
}
}
@ -971,55 +1177,35 @@ namespace MediaBrowser.Controller.MediaEncoding
if (!string.IsNullOrEmpty(level))
{
level = NormalizeTranscodingLevel(state.OutputVideoCodec, level);
level = NormalizeTranscodingLevel(state, level);
// h264_qsv and h264_nvenc expect levels to be expressed as a decimal. libx264 supports decimal and non-decimal format
// also needed for libx264 due to https://trac.ffmpeg.org/ticket/3307
// libx264, QSV, AMF, VAAPI can adjust the given level to match the output.
if (string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase))
{
switch (level)
param += " -level " + level;
}
else if (string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
// hevc_qsv use -level 51 instead of -level 153.
if (double.TryParse(level, NumberStyles.Any, _usCulture, out double hevcLevel))
{
case "30":
param += " -level 3.0";
break;
case "31":
param += " -level 3.1";
break;
case "32":
param += " -level 3.2";
break;
case "40":
param += " -level 4.0";
break;
case "41":
param += " -level 4.1";
break;
case "42":
param += " -level 4.2";
break;
case "50":
param += " -level 5.0";
break;
case "51":
param += " -level 5.1";
break;
case "52":
param += " -level 5.2";
break;
default:
param += " -level " + level;
break;
param += " -level " + hevcLevel / 3;
}
}
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_amf", StringComparison.OrdinalIgnoreCase))
{
// nvenc doesn't decode with param -level set ?!
// TODO:
param += " -level " + level;
}
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_nvenc", StringComparison.OrdinalIgnoreCase))
{
// level option may cause NVENC to fail.
// NVENC cannot adjust the given level, just throw an error.
}
else if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
|| !string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
param += " -level " + level;
}
@ -1032,42 +1218,11 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
{
// todo
}
if (!string.Equals(videoEncoder, "h264_omx", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
param = "-pix_fmt yuv420p " + param;
}
if (string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_amf", StringComparison.OrdinalIgnoreCase))
{
var videoStream = state.VideoStream;
var isColorDepth10 = IsColorDepth10(state);
if (isColorDepth10
&& _mediaEncoder.SupportsHwaccel("opencl")
&& encodingOptions.EnableTonemapping
&& !string.IsNullOrEmpty(videoStream.VideoRange)
&& videoStream.VideoRange.Contains("HDR", StringComparison.OrdinalIgnoreCase))
{
param = "-pix_fmt nv12 " + param;
}
else
{
param = "-pix_fmt yuv420p " + param;
}
}
if (string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
{
param = "-pix_fmt nv21 " + param;
// libx265 only accept level option in -x265-params.
// level option may cause libx265 to fail.
// libx265 cannot adjust the given level, just throw an error.
// TODO: set fine tuned params.
param += " -x265-params:0 no-info=1";
}
return param;
@ -1346,7 +1501,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .5;
return .6;
}
return 1;
@ -1380,36 +1535,48 @@ namespace MediaBrowser.Controller.MediaEncoding
public int? GetAudioBitrateParam(BaseEncodingJobOptions request, MediaStream audioStream)
{
if (audioStream == null)
{
return null;
}
if (request.AudioBitRate.HasValue)
{
// Don't encode any higher than this
return Math.Min(384000, request.AudioBitRate.Value);
}
// Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
return 128000;
return GetAudioBitrateParam(request.AudioBitRate, request.AudioCodec, audioStream);
}
public int? GetAudioBitrateParam(int? audioBitRate, MediaStream audioStream)
public int? GetAudioBitrateParam(int? audioBitRate, string audioCodec, MediaStream audioStream)
{
if (audioStream == null)
{
return null;
}
if (audioBitRate.HasValue)
if (audioBitRate.HasValue && string.IsNullOrEmpty(audioCodec))
{
// Don't encode any higher than this
return Math.Min(384000, audioBitRate.Value);
}
if (audioBitRate.HasValue && !string.IsNullOrEmpty(audioCodec))
{
if (string.Equals(audioCodec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "mp3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "ac3", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "eac3", StringComparison.OrdinalIgnoreCase))
{
if ((audioStream.Channels ?? 0) >= 6)
{
return Math.Min(640000, audioBitRate.Value);
}
return Math.Min(384000, audioBitRate.Value);
}
if (string.Equals(audioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(audioCodec, "alac", StringComparison.OrdinalIgnoreCase))
{
if ((audioStream.Channels ?? 0) >= 6)
{
return Math.Min(3584000, audioBitRate.Value);
}
return Math.Min(1536000, audioBitRate.Value);
}
}
// Empty bitrate area is not allow on iOS
// Default audio bitrate to 128K if it is not being requested
// https://ffmpeg.org/ffmpeg-codecs.html#toc-Codec-Options
@ -1447,7 +1614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
if (filters.Count > 0)
{
return "-af \"" + string.Join(",", filters) + "\"";
return " -af \"" + string.Join(",", filters) + "\"";
}
return string.Empty;
@ -1462,6 +1629,11 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{System.Int32}.</returns>
public int? GetNumAudioChannelsParam(EncodingJobInfo state, MediaStream audioStream, string outputAudioCodec)
{
if (audioStream == null)
{
return null;
}
var request = state.BaseRequest;
var inputChannels = audioStream?.Channels;
@ -1484,6 +1656,11 @@ namespace MediaBrowser.Controller.MediaEncoding
// libmp3lame currently only supports two channel output
transcoderChannelLimit = 2;
}
else if (codec.IndexOf("aac", StringComparison.OrdinalIgnoreCase) != -1)
{
// aac is able to handle 8ch(7.1 layout)
transcoderChannelLimit = 8;
}
else
{
// If we don't have any media info then limit it to 6 to prevent encoding errors due to asking for too many channels
@ -1708,7 +1885,8 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// For QSV, feed it into hardware encoder now
if (isLinux && string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
if (isLinux && (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)))
{
videoSizeParam += ",hwupload=extra_hw_frames=64";
}
@ -1729,7 +1907,8 @@ namespace MediaBrowser.Controller.MediaEncoding
: " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\"";
// When the input may or may not be hardware VAAPI decodable
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "hevc_vaapi", StringComparison.OrdinalIgnoreCase))
{
/*
[base]: HW scaling video to OutputSize
@ -1741,7 +1920,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (_mediaEncoder.SupportsHwaccel("vaapi") && videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1
&& string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase))
&& (string.Equals(outputVideoCodec, "libx264", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "libx265", StringComparison.OrdinalIgnoreCase)))
{
/*
[base]: SW scaling video to OutputSize
@ -1750,7 +1930,8 @@ namespace MediaBrowser.Controller.MediaEncoding
*/
retStr = " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"";
}
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
else if (string.Equals(outputVideoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(outputVideoCodec, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
{
/*
QSV in FFMpeg can now setup hardware overlay for transcodes.
@ -1776,7 +1957,7 @@ namespace MediaBrowser.Controller.MediaEncoding
videoSizeParam);
}
private (int? width, int? height) GetFixedOutputSize(
public static (int? width, int? height) GetFixedOutputSize(
int? videoWidth,
int? videoHeight,
int? requestedWidth,
@ -1836,7 +2017,9 @@ namespace MediaBrowser.Controller.MediaEncoding
requestedMaxHeight);
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase))
&& width.HasValue
&& height.HasValue)
{
@ -1845,7 +2028,8 @@ namespace MediaBrowser.Controller.MediaEncoding
// output dimensions. Output dimensions are guaranteed to be even.
var outputWidth = width.Value;
var outputHeight = height.Value;
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase);
var qsv_or_vaapi = string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|| string.Equals(videoEncoder, "hevc_qsv", StringComparison.OrdinalIgnoreCase);
var isDeintEnabled = state.DeInterlace("h264", true)
|| state.DeInterlace("avc", true)
|| state.DeInterlace("h265", true)
@ -2107,10 +2291,13 @@ namespace MediaBrowser.Controller.MediaEncoding
var isD3d11vaDecoder = videoDecoder.IndexOf("d3d11va", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiDecoder = videoDecoder.IndexOf("vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiH264Encoder = outputVideoCodec.IndexOf("h264_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isVaapiHevcEncoder = outputVideoCodec.IndexOf("hevc_vaapi", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvH264Encoder = outputVideoCodec.IndexOf("h264_qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isQsvHevcEncoder = outputVideoCodec.IndexOf("hevc_qsv", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecH264Decoder = videoDecoder.IndexOf("h264_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isNvdecHevcDecoder = videoDecoder.IndexOf("hevc_cuvid", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX264Encoder = outputVideoCodec.IndexOf("libx264", StringComparison.OrdinalIgnoreCase) != -1;
var isLibX265Encoder = outputVideoCodec.IndexOf("libx265", StringComparison.OrdinalIgnoreCase) != -1;
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
var isColorDepth10 = IsColorDepth10(state);
@ -2185,6 +2372,7 @@ namespace MediaBrowser.Controller.MediaEncoding
filters.Add("hwdownload");
if (isLibX264Encoder
|| isLibX265Encoder
|| hasGraphicalSubs
|| (isNvdecHevcDecoder && isDeinterlaceHevc)
|| (!isNvdecHevcDecoder && isDeinterlaceH264 || isDeinterlaceHevc))
@ -2195,20 +2383,20 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// When the input may or may not be hardware VAAPI decodable
if (isVaapiH264Encoder)
if (isVaapiH264Encoder || isVaapiHevcEncoder)
{
filters.Add("format=nv12|vaapi");
filters.Add("hwupload");
}
// When burning in graphical subtitles using overlay_qsv, upload videostream to the same qsv context
else if (isLinux && hasGraphicalSubs && isQsvH264Encoder)
else if (isLinux && hasGraphicalSubs && (isQsvH264Encoder || isQsvHevcEncoder))
{
filters.Add("hwupload=extra_hw_frames=64");
}
// If we're hardware VAAPI decoding and software encoding, download frames from the decoder first
else if (IsVaapiSupported(state) && isVaapiDecoder && isLibX264Encoder)
else if (IsVaapiSupported(state) && isVaapiDecoder && (isLibX264Encoder || isLibX265Encoder))
{
var codec = videoStream.Codec.ToLowerInvariant();
@ -2250,7 +2438,9 @@ namespace MediaBrowser.Controller.MediaEncoding
// Add software deinterlace filter before scaling filter
if ((isDeinterlaceH264 || isDeinterlaceHevc)
&& !isVaapiH264Encoder
&& !isVaapiHevcEncoder
&& !isQsvH264Encoder
&& !isQsvHevcEncoder
&& !isNvdecH264Decoder)
{
if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase))
@ -2289,7 +2479,7 @@ namespace MediaBrowser.Controller.MediaEncoding
}
// Add parameters to use VAAPI with burn-in text subtitles (GH issue #642)
if (isVaapiH264Encoder)
if (isVaapiH264Encoder || isVaapiHevcEncoder)
{
if (hasTextSubs)
{
@ -2329,7 +2519,8 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <summary>
/// Gets the number of threads.
/// </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))
{
@ -2339,17 +2530,21 @@ namespace MediaBrowser.Controller.MediaEncoding
return Math.Max(Environment.ProcessorCount - 1, 1);
}
var threads = state.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
var threads = state?.BaseRequest.CpuCoreLimit ?? encodingOptions.EncodingThreadCount;
// Automatic
if (threads <= 0 || threads >= Environment.ProcessorCount)
if (threads <= 0)
{
return 0;
}
else if (threads >= Environment.ProcessorCount)
{
return Environment.ProcessorCount;
}
return threads;
}
#nullable disable
public void TryStreamCopy(EncodingJobInfo state)
{
if (state.VideoStream != null && CanStreamCopyVideo(state, state.VideoStream))
@ -2557,6 +2752,7 @@ namespace MediaBrowser.Controller.MediaEncoding
public void AttachMediaSourceInfo(
EncodingJobInfo state,
EncodingOptions encodingOptions,
MediaSourceInfo mediaSource,
string requestedUrl)
{
@ -2687,11 +2883,23 @@ namespace MediaBrowser.Controller.MediaEncoding
request.AudioCodec = state.SupportedAudioCodecs.FirstOrDefault(i => _mediaEncoder.CanEncodeToAudioCodec(i))
?? state.SupportedAudioCodecs.FirstOrDefault();
}
var supportedVideoCodecs = state.SupportedVideoCodecs;
if (request != null && supportedVideoCodecs != null && supportedVideoCodecs.Length > 0)
{
var supportedVideoCodecsList = supportedVideoCodecs.ToList();
ShiftVideoCodecsIfNeeded(supportedVideoCodecsList, encodingOptions);
state.SupportedVideoCodecs = supportedVideoCodecsList.ToArray();
request.VideoCodec = state.SupportedVideoCodecs.FirstOrDefault();
}
}
private void ShiftAudioCodecsIfNeeded(List<string> audioCodecs, MediaStream audioStream)
{
// Nothing to do here
// No need to shift if there is only one supported audio codec.
if (audioCodecs.Count < 2)
{
return;
@ -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)
{
if (state.SubtitleStream == null || state.SubtitleDeliveryMethod != SubtitleDeliveryMethod.Embed)
@ -2752,7 +2988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var videoType = state.MediaSource.VideoType ?? VideoType.VideoFile;
// 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
// 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)
{
return null;
@ -3352,7 +3588,7 @@ namespace MediaBrowser.Controller.MediaEncoding
args += " -ar " + state.OutputAudioSampleRate.Value.ToString(_usCulture);
}
args += " " + GetAudioFilterParam(state, encodingOptions, false);
args += GetAudioFilterParam(state, encodingOptions, false);
return args;
}

View file

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

View file

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

View file

@ -64,6 +64,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
private string _ffmpegPath = string.Empty;
private string _ffprobePath;
private int threads;
public MediaEncoder(
ILogger<MediaEncoder> logger,
@ -129,6 +130,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
SetAvailableDecoders(validator.GetDecoders());
SetAvailableEncoders(validator.GetEncoders());
SetAvailableHwaccels(validator.GetHwaccels());
threads = EncodingHelper.GetNumberOfThreads(null, _configurationManager.GetEncodingOptions(), null);
}
_logger.LogInformation("FFmpeg: {EncoderLocation}: {FfmpegPath}", EncoderLocation, _ffmpegPath ?? string.Empty);
@ -377,9 +379,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
CancellationToken cancellationToken)
{
var args = extractChapters
? "{0} -i {1} -threads 0 -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";
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath).Trim();
? "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_chapters -show_format"
: "{0} -i {1} -threads {2} -v warning -print_format json -show_streams -show_format";
args = string.Format(CultureInfo.InvariantCulture, args, probeSizeArgument, inputPath, threads).Trim();
var process = new Process
{
@ -520,29 +522,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + ".jpg");
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
var vf = "scale=600:trunc(600/dar/2)*2";
var vf = string.Empty;
if (threedFormat.HasValue)
{
switch (threedFormat.Value)
{
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";
// 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.
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. Work out the correct height based on the display aspect it will maintain the aspect where -1 in this case (3d) may not.
break;
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";
// fsbs crop width in half,set the display aspect,crop out any black bars we may have made the scale width to 600.
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
break;
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";
// 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
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
break;
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";
// ftab crop heigt in half, set the display aspect,crop out any black bars we may have made the scale width to 600
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
break;
default:
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.
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) :
string.Format(CultureInfo.InvariantCulture, "-i {0}{3} -threads 0 -v quiet -vframes 1 -vf \"{2}\" -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg);
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 {4} -v quiet -vframes 1 {2} -f image2 \"{1}\"", inputPath, tempExtractPath, vf, mapArg, threads);
var probeSizeArgument = EncodingHelper.GetProbeSizeArgument(1);
var analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);
@ -693,7 +695,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
Directory.CreateDirectory(targetDirectory);
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 analyzeDurationArgument = EncodingHelper.GetAnalyzeDurationArgument(1);

View file

@ -234,8 +234,8 @@ namespace MediaBrowser.MediaEncoding.Probing
var channelsValue = channels.Value;
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase) ||
string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
if (string.Equals(codec, "aac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "mp3", StringComparison.OrdinalIgnoreCase))
{
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;
}
@ -774,6 +802,35 @@ namespace MediaBrowser.MediaEncoding.Probing
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;
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)
{
if (data.Format != null)

View file

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

View file

@ -15,7 +15,7 @@ namespace MediaBrowser.Model.Dlna
new ResolutionConfiguration(720, 950000),
new ResolutionConfiguration(1280, 2500000),
new ResolutionConfiguration(1920, 4000000),
new ResolutionConfiguration(2560, 8000000),
new ResolutionConfiguration(2560, 20000000),
new ResolutionConfiguration(3840, 35000000)
};
@ -29,7 +29,7 @@ namespace MediaBrowser.Model.Dlna
int? maxWidth,
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 (maxWidth.HasValue || maxHeight.HasValue)
@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
private static double GetVideoBitrateScaleFactor(string codec)
{
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase) ||
string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase) ||
string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
if (string.Equals(codec, "h265", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "hevc", StringComparison.OrdinalIgnoreCase)
|| string.Equals(codec, "vp9", StringComparison.OrdinalIgnoreCase))
{
return .5;
return .6;
}
return 1;

View file

@ -872,11 +872,34 @@ namespace MediaBrowser.Model.Dlna
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;
@ -897,14 +920,27 @@ namespace MediaBrowser.Model.Dlna
}
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
defaultBitrate = targetAudioChannels.Value < 2 ? 128000 : 192000;
// Reduce the bitrate if we're downmixing.
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
{
defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrateIfUnknown(audioStream);
defaultBitrate = audioStream.BitRate ?? GetDefaultAudioBitrate(targetAudioCodec, targetAudioChannels);
}
// Seeing webm encoding failures when source has 1 audio channel and 22k bitrate.
@ -938,8 +974,28 @@ namespace MediaBrowser.Model.Dlna
{
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(

View file

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

View file

@ -2,6 +2,7 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
namespace MediaBrowser.Model.Playlists
{
@ -9,15 +10,10 @@ namespace MediaBrowser.Model.Playlists
{
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 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))
{
// Safeguard against incoming data having an emtpy name
// Safeguard against incoming data having an empty name
if (!string.IsNullOrWhiteSpace(source.Name))
{
target.Name = source.Name;
@ -48,7 +48,7 @@ namespace MediaBrowser.Providers.Manager
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))
{
target.OriginalTitle = source.OriginalTitle;

View file

@ -50,7 +50,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
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)
{
item.Name = result.Title;
@ -151,7 +151,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
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)
{
item.Name = result.Title;
@ -385,7 +385,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
var isConfiguredForEnglish = IsConfiguredForEnglish(item) || _configurationManager.Configuration.EnableNewOmdbSupport;
// 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))
{
item.Genres = Array.Empty<string>();
@ -401,7 +401,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
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;
}
@ -455,7 +455,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
{
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);
}

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