mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-01-23 03:28:09 +00:00
Merge branch 'master' into keyframe_extraction_v1
# Conflicts: # Jellyfin.Api/Controllers/DynamicHlsController.cs # MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs # MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -7,11 +6,14 @@ namespace MediaBrowser.Model.Channels
|
||||
{
|
||||
public class ChannelFeatures
|
||||
{
|
||||
public ChannelFeatures()
|
||||
public ChannelFeatures(string name, Guid id)
|
||||
{
|
||||
MediaTypes = Array.Empty<ChannelMediaType>();
|
||||
ContentTypes = Array.Empty<ChannelMediaContentType>();
|
||||
DefaultSortFields = Array.Empty<ChannelItemSortField>();
|
||||
|
||||
Name = name;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -24,7 +26,7 @@ namespace MediaBrowser.Model.Channels
|
||||
/// Gets or sets the identifier.
|
||||
/// </summary>
|
||||
/// <value>The identifier.</value>
|
||||
public string Id { get; set; }
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance can search.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -13,13 +12,13 @@ namespace MediaBrowser.Model.Channels
|
||||
/// Gets or sets the fields to return within the items, in addition to basic information.
|
||||
/// </summary>
|
||||
/// <value>The fields.</value>
|
||||
public ItemFields[] Fields { get; set; }
|
||||
public ItemFields[]? Fields { get; set; }
|
||||
|
||||
public bool? EnableImages { get; set; }
|
||||
|
||||
public int? ImageTypeLimit { get; set; }
|
||||
|
||||
public ImageType[] EnableImageTypes { get; set; }
|
||||
public ImageType[]? EnableImageTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user identifier.
|
||||
|
||||
75
MediaBrowser.Model/ClientLog/ClientLogEvent.cs
Normal file
75
MediaBrowser.Model/ClientLog/ClientLogEvent.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Model.ClientLog
|
||||
{
|
||||
/// <summary>
|
||||
/// The client log event.
|
||||
/// </summary>
|
||||
public class ClientLogEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientLogEvent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The log timestamp.</param>
|
||||
/// <param name="level">The log level.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="clientName">The client name.</param>
|
||||
/// <param name="clientVersion">The client version.</param>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
public ClientLogEvent(
|
||||
DateTime timestamp,
|
||||
LogLevel level,
|
||||
Guid? userId,
|
||||
string clientName,
|
||||
string clientVersion,
|
||||
string deviceId,
|
||||
string message)
|
||||
{
|
||||
Timestamp = timestamp;
|
||||
UserId = userId;
|
||||
ClientName = clientName;
|
||||
ClientVersion = clientVersion;
|
||||
DeviceId = deviceId;
|
||||
Message = message;
|
||||
Level = level;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the event timestamp.
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log level.
|
||||
/// </summary>
|
||||
public LogLevel Level { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user id.
|
||||
/// </summary>
|
||||
public Guid? UserId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client name.
|
||||
/// </summary>
|
||||
public string ClientName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the client version.
|
||||
/// </summary>
|
||||
public string ClientVersion { get; }
|
||||
|
||||
///
|
||||
/// <summary>
|
||||
/// Gets the device id.
|
||||
/// </summary>
|
||||
public string DeviceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log message.
|
||||
/// </summary>
|
||||
public string Message { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
@@ -35,21 +34,21 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets the cache path.
|
||||
/// </summary>
|
||||
/// <value>The cache path.</value>
|
||||
public string CachePath { get; set; }
|
||||
public string? CachePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last known version that was ran using the configuration.
|
||||
/// </summary>
|
||||
/// <value>The version from previous run.</value>
|
||||
[XmlIgnore]
|
||||
public Version PreviousVersion { get; set; }
|
||||
public Version? PreviousVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the stringified PreviousVersion to be stored/loaded,
|
||||
/// because System.Version itself isn't xml-serializable.
|
||||
/// </summary>
|
||||
/// <value>String value of PreviousVersion.</value>
|
||||
public string PreviousVersionStr
|
||||
public string? PreviousVersionStr
|
||||
{
|
||||
get => PreviousVersion?.ToString();
|
||||
set
|
||||
|
||||
@@ -18,12 +18,9 @@ namespace MediaBrowser.Model.Configuration
|
||||
// This is a DRM device that is almost guaranteed to be there on every intel platform,
|
||||
// plus it's the default one in ffmpeg if you don't specify anything
|
||||
VaapiDevice = "/dev/dri/renderD128";
|
||||
// This is the OpenCL device that is used for tonemapping.
|
||||
// The left side of the dot is the platform number, and the right side is the device number on the platform.
|
||||
OpenclDevice = "0.0";
|
||||
EnableTonemapping = false;
|
||||
EnableVppTonemapping = false;
|
||||
TonemappingAlgorithm = "hable";
|
||||
TonemappingAlgorithm = "bt2390";
|
||||
TonemappingRange = "auto";
|
||||
TonemappingDesat = 0;
|
||||
TonemappingThreshold = 0.8;
|
||||
@@ -36,6 +33,9 @@ namespace MediaBrowser.Model.Configuration
|
||||
EnableDecodingColorDepth10Hevc = true;
|
||||
EnableDecodingColorDepth10Vp9 = true;
|
||||
EnableEnhancedNvdecDecoder = true;
|
||||
PreferSystemNativeHwDecoder = true;
|
||||
EnableIntelLowPowerH264HwEncoder = false;
|
||||
EnableIntelLowPowerHevcHwEncoder = false;
|
||||
EnableHardwareEncoding = true;
|
||||
AllowHevcEncoding = false;
|
||||
EnableSubtitleExtraction = true;
|
||||
@@ -73,8 +73,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public string VaapiDevice { get; set; }
|
||||
|
||||
public string OpenclDevice { get; set; }
|
||||
|
||||
public bool EnableTonemapping { get; set; }
|
||||
|
||||
public bool EnableVppTonemapping { get; set; }
|
||||
@@ -107,6 +105,12 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool EnableEnhancedNvdecDecoder { get; set; }
|
||||
|
||||
public bool PreferSystemNativeHwDecoder { get; set; }
|
||||
|
||||
public bool EnableIntelLowPowerH264HwEncoder { get; set; }
|
||||
|
||||
public bool EnableIntelLowPowerHevcHwEncoder { get; set; }
|
||||
|
||||
public bool EnableHardwareEncoding { get; set; }
|
||||
|
||||
public bool AllowHevcEncoding { get; set; }
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -17,11 +16,11 @@ namespace MediaBrowser.Model.Configuration
|
||||
SkipSubtitlesIfAudioTrackMatches = true;
|
||||
RequirePerfectSubtitleMatch = true;
|
||||
|
||||
AutomaticallyAddToCollection = true;
|
||||
EnablePhotos = true;
|
||||
SaveSubtitlesWithMedia = true;
|
||||
EnableRealtimeMonitor = true;
|
||||
PathInfos = Array.Empty<MediaPathInfo>();
|
||||
EnableInternetProviders = true;
|
||||
EnableAutomaticSeriesGrouping = true;
|
||||
SeasonZeroDisplayName = "Specials";
|
||||
}
|
||||
@@ -38,6 +37,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool SaveLocalMetadata { get; set; }
|
||||
|
||||
[Obsolete("Disable remote providers in TypeOptions instead")]
|
||||
public bool EnableInternetProviders { get; set; }
|
||||
|
||||
public bool EnableAutomaticSeriesGrouping { get; set; }
|
||||
@@ -52,21 +52,21 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets the preferred metadata language.
|
||||
/// </summary>
|
||||
/// <value>The preferred metadata language.</value>
|
||||
public string PreferredMetadataLanguage { get; set; }
|
||||
public string? PreferredMetadataLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata country code.
|
||||
/// </summary>
|
||||
/// <value>The metadata country code.</value>
|
||||
public string MetadataCountryCode { get; set; }
|
||||
public string? MetadataCountryCode { get; set; }
|
||||
|
||||
public string SeasonZeroDisplayName { get; set; }
|
||||
|
||||
public string[] MetadataSavers { get; set; }
|
||||
public string[]? MetadataSavers { get; set; }
|
||||
|
||||
public string[] DisabledLocalMetadataReaders { get; set; }
|
||||
|
||||
public string[] LocalMetadataReaderOrder { get; set; }
|
||||
public string[]? LocalMetadataReaderOrder { get; set; }
|
||||
|
||||
public string[] DisabledSubtitleFetchers { get; set; }
|
||||
|
||||
@@ -76,15 +76,17 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool SkipSubtitlesIfAudioTrackMatches { get; set; }
|
||||
|
||||
public string[] SubtitleDownloadLanguages { get; set; }
|
||||
public string[]? SubtitleDownloadLanguages { get; set; }
|
||||
|
||||
public bool RequirePerfectSubtitleMatch { get; set; }
|
||||
|
||||
public bool SaveSubtitlesWithMedia { get; set; }
|
||||
|
||||
public bool AutomaticallyAddToCollection { get; set; }
|
||||
|
||||
public TypeOptions[] TypeOptions { get; set; }
|
||||
|
||||
public TypeOptions GetTypeOptions(string type)
|
||||
public TypeOptions? GetTypeOptions(string type)
|
||||
{
|
||||
foreach (var options in TypeOptions)
|
||||
{
|
||||
|
||||
@@ -13,18 +13,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// </summary>
|
||||
public class ServerConfiguration : BaseApplicationConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// The default value for <see cref="HttpServerPortNumber"/>.
|
||||
/// </summary>
|
||||
public const int DefaultHttpPort = 8096;
|
||||
|
||||
/// <summary>
|
||||
/// The default value for <see cref="PublicHttpsPort"/> and <see cref="HttpsPortNumber"/>.
|
||||
/// </summary>
|
||||
public const int DefaultHttpsPort = 8920;
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerConfiguration" /> class.
|
||||
/// </summary>
|
||||
@@ -75,149 +63,13 @@ namespace MediaBrowser.Model.Configuration
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable automatic port forwarding.
|
||||
/// </summary>
|
||||
public bool EnableUPnP { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable prometheus metrics exporting.
|
||||
/// </summary>
|
||||
public bool EnableMetrics { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public mapped port.
|
||||
/// </summary>
|
||||
/// <value>The public mapped port.</value>
|
||||
public int PublicPort { get; set; } = DefaultHttpPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the http port should be mapped as part of UPnP automatic port forwarding.
|
||||
/// </summary>
|
||||
public bool UPnPCreateHttpPortMap { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets client udp port range.
|
||||
/// </summary>
|
||||
public string UDPPortRange { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether IPV6 capability is enabled.
|
||||
/// </summary>
|
||||
public bool EnableIPV6 { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether IPV4 capability is enabled.
|
||||
/// </summary>
|
||||
public bool EnableIPV4 { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether detailed ssdp logs are sent to the console/log.
|
||||
/// "Emby.Dlna": "Debug" must be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public bool EnableSSDPTracing { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether an IP address is to be used to filter the detailed ssdp logs that are being sent to the console/log.
|
||||
/// If the setting "Emby.Dlna": "Debug" msut be set in logging.default.json for this property to work.
|
||||
/// </summary>
|
||||
public string SSDPTracingFilter { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of times SSDP UDP messages are sent.
|
||||
/// </summary>
|
||||
public int UDPSendCount { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the delay between each groups of SSDP messages (in ms).
|
||||
/// </summary>
|
||||
public int UDPSendDelay { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be Ignore for the purposes of binding.
|
||||
/// </summary>
|
||||
public bool IgnoreVirtualInterfaces { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating the interfaces that should be ignored. The list can be comma separated. <seealso cref="IgnoreVirtualInterfaces"/>.
|
||||
/// </summary>
|
||||
public string VirtualInterfaceNames { get; set; } = "vEthernet*";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time (in seconds) between the pings of SSDP gateway monitor.
|
||||
/// </summary>
|
||||
public int GatewayMonitorPeriod { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether multi-socket binding is available.
|
||||
/// </summary>
|
||||
public bool EnableMultiSocketBinding { get; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether all IPv6 interfaces should be treated as on the internal network.
|
||||
/// Depending on the address range implemented ULA ranges might not be used.
|
||||
/// </summary>
|
||||
public bool TrustAllIP6Interfaces { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ports that HDHomerun uses.
|
||||
/// </summary>
|
||||
public string HDHomerunPortRange { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets PublishedServerUri to advertise for specific subnets.
|
||||
/// </summary>
|
||||
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Autodiscovery tracing is enabled.
|
||||
/// </summary>
|
||||
public bool AutoDiscoveryTracing { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Autodiscovery is enabled.
|
||||
/// </summary>
|
||||
public bool AutoDiscovery { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the public HTTPS port.
|
||||
/// </summary>
|
||||
/// <value>The public HTTPS port.</value>
|
||||
public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP server port number.
|
||||
/// </summary>
|
||||
/// <value>The HTTP server port number.</value>
|
||||
public int HttpServerPortNumber { get; set; } = DefaultHttpPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTPS server port number.
|
||||
/// </summary>
|
||||
/// <value>The HTTPS server port number.</value>
|
||||
public int HttpsPortNumber { get; set; } = DefaultHttpsPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use HTTPS.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
|
||||
/// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
|
||||
/// </remarks>
|
||||
public bool EnableHttps { get; set; } = false;
|
||||
|
||||
public bool EnableNormalizedItemByNameIds { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
|
||||
/// </summary>
|
||||
public string CertificatePath { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
|
||||
/// </summary>
|
||||
public string CertificatePassword { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is port authorized.
|
||||
/// </summary>
|
||||
@@ -229,11 +81,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// </summary>
|
||||
public bool QuickConnectAvailable { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether access outside of the LAN is permitted.
|
||||
/// </summary>
|
||||
public bool EnableRemoteAccess { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [enable case sensitive item ids].
|
||||
/// </summary>
|
||||
@@ -318,13 +165,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// <value>The file watcher delay.</value>
|
||||
public int LibraryMonitorDelay { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [enable dashboard response caching].
|
||||
/// Allows potential contributors without visual studio to modify production dashboard code and test changes.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [enable dashboard response caching]; otherwise, <c>false</c>.</value>
|
||||
public bool EnableDashboardResponseCaching { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the image saving convention.
|
||||
/// </summary>
|
||||
@@ -337,36 +177,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public string ServerName { get; set; } = string.Empty;
|
||||
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
set
|
||||
{
|
||||
// Normalize the start of the string
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
// If baseUrl is empty, set an empty prefix string
|
||||
_baseUrl = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
if (value[0] != '/')
|
||||
{
|
||||
// If baseUrl was not configured with a leading slash, append one for consistency
|
||||
value = "/" + value;
|
||||
}
|
||||
|
||||
// Normalize the end of the string
|
||||
if (value[value.Length - 1] == '/')
|
||||
{
|
||||
// If baseUrl was configured with a trailing slash, remove it for consistency
|
||||
value = value.Remove(value.Length - 1);
|
||||
}
|
||||
|
||||
_baseUrl = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string UICulture { get; set; } = "en-US";
|
||||
|
||||
public bool SaveMetadataHidden { get; set; } = false;
|
||||
@@ -381,45 +191,16 @@ namespace MediaBrowser.Model.Configuration
|
||||
|
||||
public bool DisplaySpecialsWithinSeasons { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subnets that are deemed to make up the LAN.
|
||||
/// </summary>
|
||||
public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
|
||||
/// </summary>
|
||||
public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
|
||||
|
||||
public string[] CodecsUsed { get; set; } = Array.Empty<string>();
|
||||
|
||||
public List<RepositoryInfo> PluginRepositories { get; set; } = new List<RepositoryInfo>();
|
||||
|
||||
public bool EnableExternalContentInSuggestions { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the server should force connections over HTTPS.
|
||||
/// </summary>
|
||||
public bool RequireHttps { get; set; } = false;
|
||||
|
||||
public bool EnableNewOmdbSupport { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
|
||||
/// </summary>
|
||||
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
|
||||
/// </summary>
|
||||
public bool IsRemoteIPFilterBlacklist { get; set; } = false;
|
||||
|
||||
public int ImageExtractionTimeoutMs { get; set; } = 0;
|
||||
|
||||
public PathSubstitution[] PathSubstitutions { get; set; } = Array.Empty<PathSubstitution>();
|
||||
|
||||
public string[] UninstalledPlugins { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether slow server responses should be logged as a warning.
|
||||
/// </summary>
|
||||
@@ -435,11 +216,6 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// </summary>
|
||||
public string[] CorsHosts { get; set; } = new[] { "*" };
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the known proxies.
|
||||
/// </summary>
|
||||
public string[] KnownProxies { get; set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of days we should retain activity logs.
|
||||
/// </summary>
|
||||
@@ -459,5 +235,10 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder.
|
||||
/// </summary>
|
||||
public bool RemoveOldPlugins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether clients should be allowed to upload logs.
|
||||
/// </summary>
|
||||
public bool AllowClientLogUpload { get; set; } = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -33,7 +32,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets the audio language preference.
|
||||
/// </summary>
|
||||
/// <value>The audio language preference.</value>
|
||||
public string AudioLanguagePreference { get; set; }
|
||||
public string? AudioLanguagePreference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [play default audio track].
|
||||
@@ -45,7 +44,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
/// Gets or sets the subtitle language preference.
|
||||
/// </summary>
|
||||
/// <value>The subtitle language preference.</value>
|
||||
public string SubtitleLanguagePreference { get; set; }
|
||||
public string? SubtitleLanguagePreference { get; set; }
|
||||
|
||||
public bool DisplayMissingEpisodes { get; set; }
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Configuration
|
||||
@@ -13,7 +12,7 @@ namespace MediaBrowser.Model.Configuration
|
||||
EnablePathSubstitution = true;
|
||||
}
|
||||
|
||||
public string UserId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
|
||||
public string ReleaseDateFormat { get; set; }
|
||||
|
||||
|
||||
23
MediaBrowser.Model/Cryptography/Constants.cs
Normal file
23
MediaBrowser.Model/Cryptography/Constants.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
/// <summary>
|
||||
/// Class containing global constants for Jellyfin Cryptography.
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
/// <summary>
|
||||
/// The default length for new salts.
|
||||
/// </summary>
|
||||
public const int DefaultSaltLength = 128 / 8;
|
||||
|
||||
/// <summary>
|
||||
/// The default output length.
|
||||
/// </summary>
|
||||
public const int DefaultOutputLength = 512 / 8;
|
||||
|
||||
/// <summary>
|
||||
/// The default amount of iterations for hashing passwords.
|
||||
/// </summary>
|
||||
public const int DefaultIterations = 120000;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
@@ -8,11 +8,14 @@ namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
string DefaultHashMethod { get; }
|
||||
|
||||
IEnumerable<string> GetSupportedHashMethods();
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="PasswordHash" /> instance.
|
||||
/// </summary>
|
||||
/// <param name="password">The password that will be hashed.</param>
|
||||
/// <returns>A <see cref="PasswordHash" /> instance with the hash method, hash, salt and number of iterations.</returns>
|
||||
PasswordHash CreatePasswordHash(ReadOnlySpan<char> password);
|
||||
|
||||
byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt);
|
||||
|
||||
byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt);
|
||||
bool Verify(PasswordHash hash, ReadOnlySpan<char> password);
|
||||
|
||||
byte[] GenerateSalt();
|
||||
|
||||
|
||||
219
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
219
MediaBrowser.Model/Cryptography/PasswordHash.cs
Normal file
@@ -0,0 +1,219 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace MediaBrowser.Model.Cryptography
|
||||
{
|
||||
// Defined from this hash storage spec
|
||||
// https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md
|
||||
// $<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]]
|
||||
// with one slight amendment to ease the transition, we're writing out the bytes in hex
|
||||
// rather than making them a BASE64 string with stripped padding
|
||||
public class PasswordHash
|
||||
{
|
||||
private readonly Dictionary<string, string> _parameters;
|
||||
private readonly byte[] _salt;
|
||||
private readonly byte[] _hash;
|
||||
|
||||
public PasswordHash(string id, byte[] hash)
|
||||
: this(id, hash, Array.Empty<byte>())
|
||||
{
|
||||
}
|
||||
|
||||
public PasswordHash(string id, byte[] hash, byte[] salt)
|
||||
: this(id, hash, salt, new Dictionary<string, string>())
|
||||
{
|
||||
}
|
||||
|
||||
public PasswordHash(string id, byte[] hash, byte[] salt, Dictionary<string, string> parameters)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
if (id.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("String can't be empty", nameof(id));
|
||||
}
|
||||
|
||||
Id = id;
|
||||
_hash = hash;
|
||||
_salt = salt;
|
||||
_parameters = parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the symbolic name for the function used.
|
||||
/// </summary>
|
||||
/// <value>Returns the symbolic name for the function used.</value>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional parameters used by the hash function.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Parameters => _parameters;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the salt used for hashing the password.
|
||||
/// </summary>
|
||||
/// <value>Returns the salt used for hashing the password.</value>
|
||||
public ReadOnlySpan<byte> Salt => _salt;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hashed password.
|
||||
/// </summary>
|
||||
/// <value>Return the hashed password.</value>
|
||||
public ReadOnlySpan<byte> Hash => _hash;
|
||||
|
||||
public static PasswordHash Parse(ReadOnlySpan<char> hashString)
|
||||
{
|
||||
if (hashString.IsEmpty)
|
||||
{
|
||||
throw new ArgumentException("String can't be empty", nameof(hashString));
|
||||
}
|
||||
|
||||
if (hashString[0] != '$')
|
||||
{
|
||||
throw new FormatException("Hash string must start with a $");
|
||||
}
|
||||
|
||||
// Ignore first $
|
||||
hashString = hashString[1..];
|
||||
|
||||
int nextSegment = hashString.IndexOf('$');
|
||||
if (hashString.IsEmpty || nextSegment == 0)
|
||||
{
|
||||
throw new FormatException("Hash string must contain a valid id");
|
||||
}
|
||||
else if (nextSegment == -1)
|
||||
{
|
||||
return new PasswordHash(hashString.ToString(), Array.Empty<byte>());
|
||||
}
|
||||
|
||||
ReadOnlySpan<char> id = hashString[..nextSegment];
|
||||
hashString = hashString[(nextSegment + 1)..];
|
||||
Dictionary<string, string>? parameters = null;
|
||||
|
||||
nextSegment = hashString.IndexOf('$');
|
||||
|
||||
// Optional parameters
|
||||
ReadOnlySpan<char> parametersSpan = nextSegment == -1 ? hashString : hashString[..nextSegment];
|
||||
if (parametersSpan.Contains('='))
|
||||
{
|
||||
while (!parametersSpan.IsEmpty)
|
||||
{
|
||||
ReadOnlySpan<char> parameter;
|
||||
int index = parametersSpan.IndexOf(',');
|
||||
if (index == -1)
|
||||
{
|
||||
parameter = parametersSpan;
|
||||
parametersSpan = ReadOnlySpan<char>.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
parameter = parametersSpan[..index];
|
||||
parametersSpan = parametersSpan[(index + 1)..];
|
||||
}
|
||||
|
||||
int splitIndex = parameter.IndexOf('=');
|
||||
if (splitIndex == -1 || splitIndex == 0 || splitIndex == parameter.Length - 1)
|
||||
{
|
||||
throw new FormatException("Malformed parameter in password hash string");
|
||||
}
|
||||
|
||||
(parameters ??= new Dictionary<string, string>()).Add(
|
||||
parameter[..splitIndex].ToString(),
|
||||
parameter[(splitIndex + 1)..].ToString());
|
||||
}
|
||||
|
||||
if (nextSegment == -1)
|
||||
{
|
||||
// parameters can't be null here
|
||||
return new PasswordHash(id.ToString(), Array.Empty<byte>(), Array.Empty<byte>(), parameters!);
|
||||
}
|
||||
|
||||
hashString = hashString[(nextSegment + 1)..];
|
||||
nextSegment = hashString.IndexOf('$');
|
||||
}
|
||||
|
||||
if (nextSegment == 0)
|
||||
{
|
||||
throw new FormatException("Hash string contains an empty segment");
|
||||
}
|
||||
|
||||
byte[] hash;
|
||||
byte[] salt;
|
||||
|
||||
if (nextSegment == -1)
|
||||
{
|
||||
salt = Array.Empty<byte>();
|
||||
hash = Convert.FromHexString(hashString);
|
||||
}
|
||||
else
|
||||
{
|
||||
salt = Convert.FromHexString(hashString[..nextSegment]);
|
||||
hashString = hashString[(nextSegment + 1)..];
|
||||
nextSegment = hashString.IndexOf('$');
|
||||
if (nextSegment != -1)
|
||||
{
|
||||
throw new FormatException("Hash string contains too many segments");
|
||||
}
|
||||
|
||||
if (hashString.IsEmpty)
|
||||
{
|
||||
throw new FormatException("Hash segment is empty");
|
||||
}
|
||||
|
||||
hash = Convert.FromHexString(hashString);
|
||||
}
|
||||
|
||||
return new PasswordHash(id.ToString(), hash, salt, parameters ?? new Dictionary<string, string>());
|
||||
}
|
||||
|
||||
private void SerializeParameters(StringBuilder stringBuilder)
|
||||
{
|
||||
if (_parameters.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
stringBuilder.Append('$');
|
||||
foreach (var pair in _parameters)
|
||||
{
|
||||
stringBuilder.Append(pair.Key)
|
||||
.Append('=')
|
||||
.Append(pair.Value)
|
||||
.Append(',');
|
||||
}
|
||||
|
||||
// Remove last ','
|
||||
stringBuilder.Length -= 1;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var str = new StringBuilder()
|
||||
.Append('$')
|
||||
.Append(Id);
|
||||
SerializeParameters(str);
|
||||
|
||||
if (_salt.Length != 0)
|
||||
{
|
||||
str.Append('$')
|
||||
.Append(Convert.ToHexString(_salt));
|
||||
}
|
||||
|
||||
if (_hash.Length != 0)
|
||||
{
|
||||
str.Append('$')
|
||||
.Append(Convert.ToHexString(_hash));
|
||||
}
|
||||
|
||||
return str.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
@@ -58,7 +58,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
foreach (var val in codec)
|
||||
{
|
||||
if (codecs.Contains(val, StringComparer.OrdinalIgnoreCase))
|
||||
if (codecs.Contains(val, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
@@ -167,7 +167,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
switch (condition.Condition)
|
||||
{
|
||||
case ProfileConditionType.EqualsAny:
|
||||
return expected.Split('|').Contains(currentValue, StringComparer.OrdinalIgnoreCase);
|
||||
return expected.Split('|').Contains(currentValue, StringComparison.OrdinalIgnoreCase);
|
||||
case ProfileConditionType.Equals:
|
||||
return string.Equals(currentValue, expected, StringComparison.OrdinalIgnoreCase);
|
||||
case ProfileConditionType.NotEquals:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
@@ -62,7 +62,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
foreach (var container in allInputContainers)
|
||||
{
|
||||
if (profileContainers.Contains(container, StringComparer.OrdinalIgnoreCase))
|
||||
if (profileContainers.Contains(container, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return !isNegativeList;
|
||||
}
|
||||
|
||||
@@ -151,10 +151,12 @@ namespace MediaBrowser.Model.Dlna
|
||||
DlnaFlags.InteractiveTransferMode |
|
||||
DlnaFlags.DlnaV15;
|
||||
|
||||
// if (isDirectStream)
|
||||
// {
|
||||
// flagValue = flagValue | DlnaFlags.ByteBasedSeek;
|
||||
// }
|
||||
if (isDirectStream)
|
||||
{
|
||||
flagValue |= DlnaFlags.ByteBasedSeek;
|
||||
}
|
||||
|
||||
// Time based seek is curently disabled when streaming. On LG CX3 adding DlnaFlags.TimeBasedSeek and orgPn causes the DLNA playback to fail (format not supported). Further investigations are needed before enabling the remaining code paths.
|
||||
// else if (runtimeTicks.HasValue)
|
||||
// {
|
||||
// flagValue = flagValue | DlnaFlags.TimeBasedSeek;
|
||||
@@ -208,7 +210,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
if (string.IsNullOrEmpty(orgPn))
|
||||
{
|
||||
contentFeatureList.Add(orgOp.TrimStart(';') + orgCi + dlnaflags);
|
||||
continue;
|
||||
}
|
||||
else if (isDirectStream)
|
||||
{
|
||||
// orgOp should be added all the time once the time based seek is resolved for transcoded streams
|
||||
contentFeatureList.Add("DLNA.ORG_PN=" + orgPn + orgOp + orgCi + dlnaflags);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma warning disable CA1819 // Properties should not return arrays
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
@@ -253,7 +253,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -287,7 +287,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -328,7 +328,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
var audioCodecs = i.GetAudioCodecs();
|
||||
if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -469,13 +469,13 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
var audioCodecs = i.GetAudioCodecs();
|
||||
if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var videoCodecs = i.GetVideoCodecs();
|
||||
if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparer.OrdinalIgnoreCase))
|
||||
if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,20 +6,6 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
public static class DlnaMaps
|
||||
{
|
||||
private static readonly string DefaultStreaming =
|
||||
FlagsToString(DlnaFlags.StreamingTransferMode |
|
||||
DlnaFlags.BackgroundTransferMode |
|
||||
DlnaFlags.ConnectionStall |
|
||||
DlnaFlags.ByteBasedSeek |
|
||||
DlnaFlags.DlnaV15);
|
||||
|
||||
private static readonly string DefaultInteractive =
|
||||
FlagsToString(DlnaFlags.InteractiveTransferMode |
|
||||
DlnaFlags.BackgroundTransferMode |
|
||||
DlnaFlags.ConnectionStall |
|
||||
DlnaFlags.ByteBasedSeek |
|
||||
DlnaFlags.DlnaV15);
|
||||
|
||||
public static string FlagsToString(DlnaFlags flags)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
public class SortCriteria
|
||||
{
|
||||
public SortCriteria(string value)
|
||||
public SortCriteria(string sortOrder)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sortOrder) && Enum.TryParse<SortOrder>(sortOrder, true, out var sortOrderValue))
|
||||
{
|
||||
SortOrder = sortOrderValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
SortOrder = SortOrder.Ascending;
|
||||
}
|
||||
}
|
||||
|
||||
public SortOrder SortOrder => SortOrder.Ascending;
|
||||
public SortOrder SortOrder { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,8 +289,8 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
var directPlayInfo = GetAudioDirectPlayMethods(item, audioStream, options);
|
||||
|
||||
var directPlayMethods = directPlayInfo.Item1;
|
||||
var transcodeReasons = directPlayInfo.Item2.ToList();
|
||||
var directPlayMethods = directPlayInfo.PlayMethods;
|
||||
var transcodeReasons = directPlayInfo.TranscodeReasons.ToList();
|
||||
|
||||
int? inputAudioChannels = audioStream?.Channels;
|
||||
int? inputAudioBitrate = audioStream?.BitDepth;
|
||||
@@ -448,14 +448,14 @@ namespace MediaBrowser.Model.Dlna
|
||||
return options.GetMaxBitrate(isAudio);
|
||||
}
|
||||
|
||||
private (IEnumerable<PlayMethod>, IEnumerable<TranscodeReason>) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
|
||||
private (IEnumerable<PlayMethod> PlayMethods, IEnumerable<TranscodeReason> TranscodeReasons) GetAudioDirectPlayMethods(MediaSourceInfo item, MediaStream audioStream, AudioOptions options)
|
||||
{
|
||||
DirectPlayProfile directPlayProfile = options.Profile.DirectPlayProfiles
|
||||
.FirstOrDefault(x => x.Type == DlnaProfileType.Audio && IsAudioDirectPlaySupported(x, item, audioStream));
|
||||
|
||||
if (directPlayProfile == null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Profile: {0}, No audio direct play profiles found for {1} with codec {2}",
|
||||
options.Profile.Name ?? "Unknown Profile",
|
||||
item.Path ?? "Unknown path",
|
||||
@@ -677,12 +677,12 @@ namespace MediaBrowser.Model.Dlna
|
||||
var videoStream = item.VideoStream;
|
||||
|
||||
// TODO: This doesn't account for situations where the device is able to handle the media's bitrate, but the connection isn't fast enough
|
||||
var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, options, PlayMethod.DirectPlay);
|
||||
var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, options, PlayMethod.DirectStream);
|
||||
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.Item1);
|
||||
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.Item1);
|
||||
var directPlayEligibilityResult = IsEligibleForDirectPlay(item, GetBitrateForDirectPlayCheck(item, options, true) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectPlay);
|
||||
var directStreamEligibilityResult = IsEligibleForDirectPlay(item, options.GetMaxBitrate(false) ?? 0, subtitleStream, audioStream, options, PlayMethod.DirectStream);
|
||||
bool isEligibleForDirectPlay = options.EnableDirectPlay && (options.ForceDirectPlay || directPlayEligibilityResult.DirectPlay);
|
||||
bool isEligibleForDirectStream = options.EnableDirectStream && (options.ForceDirectStream || directStreamEligibilityResult.DirectPlay);
|
||||
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Profile: {0}, Path: {1}, isEligibleForDirectPlay: {2}, isEligibleForDirectStream: {3}",
|
||||
options.Profile.Name ?? "Unknown Profile",
|
||||
item.Path ?? "Unknown path",
|
||||
@@ -695,7 +695,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
// See if it can be direct played
|
||||
var directPlayInfo = GetVideoDirectPlayProfile(options, item, videoStream, audioStream, isEligibleForDirectStream);
|
||||
var directPlay = directPlayInfo.Item1;
|
||||
var directPlay = directPlayInfo.PlayMethod;
|
||||
|
||||
if (directPlay != null)
|
||||
{
|
||||
@@ -713,17 +713,17 @@ namespace MediaBrowser.Model.Dlna
|
||||
return playlistItem;
|
||||
}
|
||||
|
||||
transcodeReasons.AddRange(directPlayInfo.Item2);
|
||||
transcodeReasons.AddRange(directPlayInfo.TranscodeReasons);
|
||||
}
|
||||
|
||||
if (directPlayEligibilityResult.Item2.HasValue)
|
||||
if (directPlayEligibilityResult.Reason.HasValue)
|
||||
{
|
||||
transcodeReasons.Add(directPlayEligibilityResult.Item2.Value);
|
||||
transcodeReasons.Add(directPlayEligibilityResult.Reason.Value);
|
||||
}
|
||||
|
||||
if (directStreamEligibilityResult.Item2.HasValue)
|
||||
if (directStreamEligibilityResult.Reason.HasValue)
|
||||
{
|
||||
transcodeReasons.Add(directStreamEligibilityResult.Item2.Value);
|
||||
transcodeReasons.Add(directStreamEligibilityResult.Reason.Value);
|
||||
}
|
||||
|
||||
// Can't direct play, find the transcoding profile
|
||||
@@ -1000,7 +1000,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
return 7168000;
|
||||
}
|
||||
|
||||
private (PlayMethod?, List<TranscodeReason>) GetVideoDirectPlayProfile(
|
||||
private (PlayMethod? PlayMethod, List<TranscodeReason> TranscodeReasons) GetVideoDirectPlayProfile(
|
||||
VideoOptions options,
|
||||
MediaSourceInfo mediaSource,
|
||||
MediaStream videoStream,
|
||||
@@ -1033,7 +1033,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (directPlay == null)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Container: {Container}, Video: {Video}, Audio: {Audio} cannot be direct played by profile: {Profile} for path: {Path}",
|
||||
container,
|
||||
videoStream?.Codec ?? "no video",
|
||||
@@ -1198,7 +1198,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
private void LogConditionFailure(DeviceProfile profile, string type, ProfileCondition condition, MediaSourceInfo mediaSource)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Profile: {0}, DirectPlay=false. Reason={1}.{2} Condition: {3}. ConditionValue: {4}. IsRequired: {5}. Path: {6}",
|
||||
type,
|
||||
profile.Name ?? "Unknown Profile",
|
||||
@@ -1209,10 +1209,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
mediaSource.Path ?? "Unknown path");
|
||||
}
|
||||
|
||||
private (bool directPlay, TranscodeReason? reason) IsEligibleForDirectPlay(
|
||||
private (bool DirectPlay, TranscodeReason? Reason) IsEligibleForDirectPlay(
|
||||
MediaSourceInfo item,
|
||||
long maxBitrate,
|
||||
MediaStream subtitleStream,
|
||||
MediaStream audioStream,
|
||||
VideoOptions options,
|
||||
PlayMethod playMethod)
|
||||
{
|
||||
@@ -1220,16 +1221,27 @@ namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
var subtitleProfile = GetSubtitleProfile(item, subtitleStream, options.Profile.SubtitleProfiles, playMethod, _transcoderSupport, item.Container, null);
|
||||
|
||||
if (subtitleProfile.Method != SubtitleDeliveryMethod.External && subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
|
||||
if (subtitleProfile.Method != SubtitleDeliveryMethod.Drop
|
||||
&& subtitleProfile.Method != SubtitleDeliveryMethod.External
|
||||
&& subtitleProfile.Method != SubtitleDeliveryMethod.Embed)
|
||||
{
|
||||
_logger.LogInformation("Not eligible for {0} due to unsupported subtitles", playMethod);
|
||||
_logger.LogDebug("Not eligible for {0} due to unsupported subtitles", playMethod);
|
||||
return (false, TranscodeReason.SubtitleCodecNotSupported);
|
||||
}
|
||||
}
|
||||
|
||||
bool result = IsAudioEligibleForDirectPlay(item, maxBitrate, playMethod);
|
||||
if (!result)
|
||||
{
|
||||
return (false, TranscodeReason.ContainerBitrateExceedsLimit);
|
||||
}
|
||||
|
||||
return (result, result ? (TranscodeReason?)null : TranscodeReason.ContainerBitrateExceedsLimit);
|
||||
if (audioStream?.IsExternal == true)
|
||||
{
|
||||
return (false, TranscodeReason.AudioIsExternal);
|
||||
}
|
||||
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
public static SubtitleProfile GetSubtitleProfile(
|
||||
@@ -1404,7 +1416,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
|
||||
if (itemBitrate > requestedMaxBitrate)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
_logger.LogDebug(
|
||||
"Bitrate exceeds {PlayBackMethod} limit: media bitrate: {MediaBitrate}, max bitrate: {MaxBitrate}",
|
||||
playMethod,
|
||||
itemBitrate,
|
||||
|
||||
@@ -794,7 +794,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
// strip spaces to avoid having to encode h264 profile names
|
||||
list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty)));
|
||||
list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal)));
|
||||
}
|
||||
|
||||
if (!item.IsDirectStream)
|
||||
|
||||
@@ -25,6 +25,11 @@ namespace MediaBrowser.Model.Dlna
|
||||
/// <summary>
|
||||
/// Serve the subtitles as a separate HLS stream.
|
||||
/// </summary>
|
||||
Hls = 3
|
||||
Hls = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Drop the subtitle.
|
||||
/// </summary>
|
||||
Drop = 4
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
@@ -42,7 +42,7 @@ namespace MediaBrowser.Model.Dlna
|
||||
}
|
||||
|
||||
var languages = GetLanguages();
|
||||
return languages.Length == 0 || languages.Contains(subLanguage, StringComparer.OrdinalIgnoreCase);
|
||||
return languages.Length == 0 || languages.Contains(subLanguage, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#nullable disable
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Model.Entities
|
||||
namespace MediaBrowser.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the display preferences for any item that supports them (usually Folders).
|
||||
@@ -18,32 +17,32 @@ namespace MediaBrowser.Model.Entities
|
||||
PrimaryImageHeight = 250;
|
||||
PrimaryImageWidth = 250;
|
||||
ShowBackdrop = true;
|
||||
CustomPrefs = new Dictionary<string, string>();
|
||||
CustomPrefs = new Dictionary<string, string?>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
/// <value>The user id.</value>
|
||||
public string Id { get; set; }
|
||||
public string? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the view.
|
||||
/// </summary>
|
||||
/// <value>The type of the view.</value>
|
||||
public string ViewType { get; set; }
|
||||
public string? ViewType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sort by.
|
||||
/// </summary>
|
||||
/// <value>The sort by.</value>
|
||||
public string SortBy { get; set; }
|
||||
public string? SortBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index by.
|
||||
/// </summary>
|
||||
/// <value>The index by.</value>
|
||||
public string IndexBy { get; set; }
|
||||
public string? IndexBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [remember indexing].
|
||||
@@ -67,7 +66,7 @@ namespace MediaBrowser.Model.Entities
|
||||
/// Gets or sets the custom prefs.
|
||||
/// </summary>
|
||||
/// <value>The custom prefs.</value>
|
||||
public Dictionary<string, string> CustomPrefs { get; set; }
|
||||
public Dictionary<string, string?> CustomPrefs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the scroll direction.
|
||||
@@ -102,6 +101,6 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <summary>
|
||||
/// Gets or sets the client.
|
||||
/// </summary>
|
||||
public string Client { get; set; }
|
||||
public string? Client { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,10 @@ namespace MediaBrowser.Model.Entities
|
||||
/// <summary>
|
||||
/// The screenshot.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This enum value is obsolete.
|
||||
/// XmlSerializer does not serialize/deserialize objects that are marked as [Obsolete].
|
||||
/// </remarks>
|
||||
Screenshot = 8,
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Model.Globalization
|
||||
@@ -56,10 +55,10 @@ namespace MediaBrowser.Model.Globalization
|
||||
IEnumerable<LocalizationOption> GetLocalizationOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the correct <see cref="CultureInfo" /> for the given language.
|
||||
/// Returns the correct <see cref="CultureDto" /> for the given language.
|
||||
/// </summary>
|
||||
/// <param name="language">The language.</param>
|
||||
/// <returns>The correct <see cref="CultureInfo" /> for the given language.</returns>
|
||||
/// <returns>The correct <see cref="CultureDto" /> for the given language.</returns>
|
||||
CultureDto? FindLanguageInfo(string language);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace MediaBrowser.Model.IO
|
||||
@@ -8,6 +7,25 @@ namespace MediaBrowser.Model.IO
|
||||
/// </summary>
|
||||
public static class AsyncFile
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="FileStreamOptions"/> for reading files async.
|
||||
/// </summary>
|
||||
public static FileStreamOptions ReadOptions => new FileStreamOptions()
|
||||
{
|
||||
Options = FileOptions.Asynchronous
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default <see cref="FileStreamOptions"/> for writing files async.
|
||||
/// </summary>
|
||||
public static FileStreamOptions WriteOptions => new FileStreamOptions()
|
||||
{
|
||||
Mode = FileMode.OpenOrCreate,
|
||||
Access = FileAccess.Write,
|
||||
Share = FileShare.None,
|
||||
Options = FileOptions.Asynchronous
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Opens an existing file for reading.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,64 +9,8 @@ namespace MediaBrowser.Model.IO
|
||||
/// </summary>
|
||||
public interface IZipClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from zip.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from7z.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="sourceFile">The source file.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all from tar.
|
||||
/// </summary>
|
||||
/// <param name="source">The source.</param>
|
||||
/// <param name="targetPath">The target path.</param>
|
||||
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
|
||||
void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
@@ -34,10 +33,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="MimeTypes" Version="2.2.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -47,7 +50,7 @@
|
||||
<!-- Code Analyzers-->
|
||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" />
|
||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.MediaInfo
|
||||
{
|
||||
public static class AudioCodec
|
||||
{
|
||||
public const string AAC = "aac";
|
||||
public const string MP3 = "mp3";
|
||||
public const string AC3 = "ac3";
|
||||
|
||||
public static string GetFriendlyName(string codec)
|
||||
{
|
||||
if (codec.Length == 0)
|
||||
@@ -15,17 +13,20 @@ namespace MediaBrowser.Model.MediaInfo
|
||||
return codec;
|
||||
}
|
||||
|
||||
switch (codec.ToLowerInvariant())
|
||||
if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
case "ac3":
|
||||
return "Dolby Digital";
|
||||
case "eac3":
|
||||
return "Dolby Digital+";
|
||||
case "dca":
|
||||
return "DTS";
|
||||
default:
|
||||
return codec.ToUpperInvariant();
|
||||
return "Dolby Digital";
|
||||
}
|
||||
else if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Dolby Digital+";
|
||||
}
|
||||
else if (string.Equals(codec, "dca", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "DTS";
|
||||
}
|
||||
|
||||
return codec.ToUpperInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,6 @@ namespace MediaBrowser.Model.MediaInfo
|
||||
DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
|
||||
}
|
||||
|
||||
public LiveStreamRequest(AudioOptions options)
|
||||
{
|
||||
MaxStreamingBitrate = options.MaxBitrate;
|
||||
ItemId = options.ItemId;
|
||||
DeviceProfile = options.Profile;
|
||||
MaxAudioChannels = options.MaxAudioChannels;
|
||||
|
||||
DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
|
||||
|
||||
if (options is VideoOptions videoOptions)
|
||||
{
|
||||
AudioStreamIndex = videoOptions.AudioStreamIndex;
|
||||
SubtitleStreamIndex = videoOptions.SubtitleStreamIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public string OpenToken { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
|
||||
namespace MediaBrowser.Model.MediaInfo
|
||||
{
|
||||
public class PlaybackInfoRequest
|
||||
{
|
||||
public PlaybackInfoRequest()
|
||||
{
|
||||
EnableDirectPlay = true;
|
||||
EnableDirectStream = true;
|
||||
EnableTranscoding = true;
|
||||
AllowVideoStreamCopy = true;
|
||||
AllowAudioStreamCopy = true;
|
||||
IsPlayback = true;
|
||||
DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http };
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public long? MaxStreamingBitrate { get; set; }
|
||||
|
||||
public long? StartTimeTicks { get; set; }
|
||||
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
public int? MaxAudioChannels { get; set; }
|
||||
|
||||
public string MediaSourceId { get; set; }
|
||||
|
||||
public string LiveStreamId { get; set; }
|
||||
|
||||
public DeviceProfile DeviceProfile { get; set; }
|
||||
|
||||
public bool EnableDirectPlay { get; set; }
|
||||
|
||||
public bool EnableDirectStream { get; set; }
|
||||
|
||||
public bool EnableTranscoding { get; set; }
|
||||
|
||||
public bool AllowVideoStreamCopy { get; set; }
|
||||
|
||||
public bool AllowAudioStreamCopy { get; set; }
|
||||
|
||||
public bool IsPlayback { get; set; }
|
||||
|
||||
public bool AutoOpenLiveStream { get; set; }
|
||||
|
||||
public MediaProtocol[] DirectPlayProtocols { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ namespace MediaBrowser.Model.MediaInfo
|
||||
public const string SSA = "ssa";
|
||||
public const string ASS = "ass";
|
||||
public const string VTT = "vtt";
|
||||
public const string SUB = "sub";
|
||||
public const string SMI = "smi";
|
||||
public const string TTML = "ttml";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@ namespace MediaBrowser.Model.Net
|
||||
/// <summary>
|
||||
/// Class MimeTypes.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// For more information on MIME types:
|
||||
/// <list type="bullet">
|
||||
/// <item>http://en.wikipedia.org/wiki/Internet_media_type</item>
|
||||
/// <item>https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types</item>
|
||||
/// <item>http://www.iana.org/assignments/media-types/media-types.xhtml</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public static class MimeTypes
|
||||
{
|
||||
/// <summary>
|
||||
@@ -50,81 +59,26 @@ namespace MediaBrowser.Model.Net
|
||||
".wtv",
|
||||
};
|
||||
|
||||
// http://en.wikipedia.org/wiki/Internet_media_type
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
|
||||
// http://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
// Add more as needed
|
||||
/// <summary>
|
||||
/// Used for extensions not in <see cref="Model.MimeTypes"/> or to override them.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> _mimeTypeLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// Type application
|
||||
{ ".7z", "application/x-7z-compressed" },
|
||||
{ ".azw", "application/vnd.amazon.ebook" },
|
||||
{ ".azw3", "application/vnd.amazon.ebook" },
|
||||
{ ".cbz", "application/x-cbz" },
|
||||
{ ".cbr", "application/epub+zip" },
|
||||
{ ".eot", "application/vnd.ms-fontobject" },
|
||||
{ ".epub", "application/epub+zip" },
|
||||
{ ".js", "application/x-javascript" },
|
||||
{ ".json", "application/json" },
|
||||
{ ".m3u8", "application/x-mpegURL" },
|
||||
{ ".map", "application/x-javascript" },
|
||||
{ ".mobi", "application/x-mobipocket-ebook" },
|
||||
{ ".opf", "application/oebps-package+xml" },
|
||||
{ ".pdf", "application/pdf" },
|
||||
{ ".rar", "application/vnd.rar" },
|
||||
{ ".srt", "application/x-subrip" },
|
||||
{ ".ttml", "application/ttml+xml" },
|
||||
{ ".wasm", "application/wasm" },
|
||||
{ ".xml", "application/xml" },
|
||||
{ ".zip", "application/zip" },
|
||||
|
||||
// Type image
|
||||
{ ".bmp", "image/bmp" },
|
||||
{ ".gif", "image/gif" },
|
||||
{ ".ico", "image/vnd.microsoft.icon" },
|
||||
{ ".jpg", "image/jpeg" },
|
||||
{ ".jpeg", "image/jpeg" },
|
||||
{ ".png", "image/png" },
|
||||
{ ".svg", "image/svg+xml" },
|
||||
{ ".svgz", "image/svg+xml" },
|
||||
{ ".tbn", "image/jpeg" },
|
||||
{ ".tif", "image/tiff" },
|
||||
{ ".tiff", "image/tiff" },
|
||||
{ ".webp", "image/webp" },
|
||||
|
||||
// Type font
|
||||
{ ".ttf", "font/ttf" },
|
||||
{ ".woff", "font/woff" },
|
||||
{ ".woff2", "font/woff2" },
|
||||
|
||||
// Type text
|
||||
{ ".ass", "text/x-ssa" },
|
||||
{ ".ssa", "text/x-ssa" },
|
||||
{ ".css", "text/css" },
|
||||
{ ".csv", "text/csv" },
|
||||
{ ".edl", "text/plain" },
|
||||
{ ".rtf", "text/rtf" },
|
||||
{ ".txt", "text/plain" },
|
||||
{ ".vtt", "text/vtt" },
|
||||
{ ".html", "text/html; charset=UTF-8" },
|
||||
{ ".htm", "text/html; charset=UTF-8" },
|
||||
|
||||
// Type video
|
||||
{ ".3gp", "video/3gpp" },
|
||||
{ ".3g2", "video/3gpp2" },
|
||||
{ ".asf", "video/x-ms-asf" },
|
||||
{ ".avi", "video/x-msvideo" },
|
||||
{ ".flv", "video/x-flv" },
|
||||
{ ".mp4", "video/mp4" },
|
||||
{ ".m4s", "video/mp4" },
|
||||
{ ".m4v", "video/x-m4v" },
|
||||
{ ".mpegts", "video/mp2t" },
|
||||
{ ".mpg", "video/mpeg" },
|
||||
{ ".mkv", "video/x-matroska" },
|
||||
{ ".mov", "video/quicktime" },
|
||||
{ ".mpd", "video/vnd.mpeg.dash.mpd" },
|
||||
{ ".ogv", "video/ogg" },
|
||||
{ ".ts", "video/mp2t" },
|
||||
{ ".webm", "video/webm" },
|
||||
{ ".wmv", "video/x-ms-wmv" },
|
||||
|
||||
// Type audio
|
||||
{ ".aac", "audio/aac" },
|
||||
@@ -133,37 +87,48 @@ namespace MediaBrowser.Model.Net
|
||||
{ ".dsf", "audio/dsf" },
|
||||
{ ".dsp", "audio/dsp" },
|
||||
{ ".flac", "audio/flac" },
|
||||
{ ".m4a", "audio/mp4" },
|
||||
{ ".m4b", "audio/m4b" },
|
||||
{ ".mid", "audio/midi" },
|
||||
{ ".midi", "audio/midi" },
|
||||
{ ".mp3", "audio/mpeg" },
|
||||
{ ".oga", "audio/ogg" },
|
||||
{ ".ogg", "audio/ogg" },
|
||||
{ ".opus", "audio/ogg" },
|
||||
{ ".vorbis", "audio/vorbis" },
|
||||
{ ".wav", "audio/wav" },
|
||||
{ ".webma", "audio/webm" },
|
||||
{ ".wma", "audio/x-ms-wma" },
|
||||
{ ".wv", "audio/x-wavpack" },
|
||||
{ ".xsp", "audio/xsp" },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<string, string> _extensionLookup = CreateExtensionLookup();
|
||||
|
||||
private static Dictionary<string, string> CreateExtensionLookup()
|
||||
private static readonly Dictionary<string, string> _extensionLookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
var dict = _mimeTypeLookup
|
||||
.GroupBy(i => i.Value)
|
||||
.ToDictionary(x => x.Key, x => x.First().Key, StringComparer.OrdinalIgnoreCase);
|
||||
// Type application
|
||||
{ "application/x-cbz", ".cbz" },
|
||||
{ "application/x-javascript", ".js" },
|
||||
{ "application/xml", ".xml" },
|
||||
{ "application/x-mpegURL", ".m3u8" },
|
||||
|
||||
dict["image/jpg"] = ".jpg";
|
||||
dict["image/x-png"] = ".png";
|
||||
// Type audio
|
||||
{ "audio/aac", ".aac" },
|
||||
{ "audio/ac3", ".ac3" },
|
||||
{ "audio/dsf", ".dsf" },
|
||||
{ "audio/dsp", ".dsp" },
|
||||
{ "audio/flac", ".flac" },
|
||||
{ "audio/m4b", ".m4b" },
|
||||
{ "audio/vorbis", ".vorbis" },
|
||||
{ "audio/x-ape", ".ape" },
|
||||
{ "audio/xsp", ".xsp" },
|
||||
{ "audio/x-wavpack", ".wv" },
|
||||
|
||||
dict["audio/x-aac"] = ".aac";
|
||||
// Type image
|
||||
{ "image/jpg", ".jpg" },
|
||||
{ "image/jpeg", ".jpg" },
|
||||
{ "image/x-png", ".png" },
|
||||
|
||||
return dict;
|
||||
}
|
||||
// Type text
|
||||
{ "text/plain", ".txt" },
|
||||
{ "text/rtf", ".rtf" },
|
||||
{ "text/x-ssa", ".ssa" },
|
||||
|
||||
// Type video
|
||||
{ "video/vnd.mpeg.dash.mpd", ".mpd" },
|
||||
{ "video/x-matroska", ".mkv" },
|
||||
};
|
||||
|
||||
public static string GetMimeType(string path) => GetMimeType(path, "application/octet-stream");
|
||||
|
||||
@@ -173,7 +138,7 @@ namespace MediaBrowser.Model.Net
|
||||
/// <param name="filename">The filename to find the MIME type of.</param>
|
||||
/// <param name="defaultValue">The default value to return if no fitting MIME type is found.</param>
|
||||
/// <returns>The correct MIME type for the given filename, or <paramref name="defaultValue"/> if it wasn't found.</returns>
|
||||
[return: NotNullIfNotNullAttribute("defaultValue")]
|
||||
[return: NotNullIfNotNull("defaultValue")]
|
||||
public static string? GetMimeType(string filename, string? defaultValue = null)
|
||||
{
|
||||
if (filename.Length == 0)
|
||||
@@ -188,29 +153,15 @@ namespace MediaBrowser.Model.Net
|
||||
return result;
|
||||
}
|
||||
|
||||
if (Model.MimeTypes.TryGetMimeType(filename, out var mimeType))
|
||||
{
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
// Catch-all for all video types that don't require specific mime types
|
||||
if (_videoFileExtensions.Contains(ext))
|
||||
{
|
||||
return "video/" + ext.Substring(1);
|
||||
}
|
||||
|
||||
// Type text
|
||||
if (string.Equals(ext, ".html", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ext, ".htm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "text/html; charset=UTF-8";
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".log", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ext, ".srt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
// Misc
|
||||
if (string.Equals(ext, ".dll", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "application/octet-stream";
|
||||
return string.Concat("video/", ext.AsSpan(1));
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
@@ -231,7 +182,8 @@ namespace MediaBrowser.Model.Net
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
var extension = Model.MimeTypes.GetMimeTypeExtensions(mimeType).FirstOrDefault();
|
||||
return string.IsNullOrEmpty(extension) ? null : "." + extension;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace MediaBrowser.Model.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Enum NetworkShareType.
|
||||
/// </summary>
|
||||
public enum NetworkShareType
|
||||
{
|
||||
/// <summary>
|
||||
/// Disk share.
|
||||
/// </summary>
|
||||
Disk,
|
||||
|
||||
/// <summary>
|
||||
/// Printer share.
|
||||
/// </summary>
|
||||
Printer,
|
||||
|
||||
/// <summary>
|
||||
/// Device share.
|
||||
/// </summary>
|
||||
Device,
|
||||
|
||||
/// <summary>
|
||||
/// IPC share.
|
||||
/// </summary>
|
||||
Ipc,
|
||||
|
||||
/// <summary>
|
||||
/// Special share.
|
||||
/// </summary>
|
||||
Special
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Model.Notifications
|
||||
{
|
||||
@@ -94,7 +94,7 @@ namespace MediaBrowser.Model.Notifications
|
||||
NotificationOption opt = GetOptions(notificationType);
|
||||
|
||||
return opt == null
|
||||
|| !opt.DisabledServices.Contains(service, StringComparer.OrdinalIgnoreCase);
|
||||
|| !opt.DisabledServices.Contains(service, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool IsEnabledToMonitorUser(string type, Guid userId)
|
||||
@@ -103,7 +103,7 @@ namespace MediaBrowser.Model.Notifications
|
||||
|
||||
return opt != null
|
||||
&& opt.Enabled
|
||||
&& !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparer.OrdinalIgnoreCase);
|
||||
&& !opt.DisabledMonitorUsers.Contains(userId.ToString("N"), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public bool IsEnabledToSendToUser(string type, string userId, User user)
|
||||
@@ -122,7 +122,7 @@ namespace MediaBrowser.Model.Notifications
|
||||
return true;
|
||||
}
|
||||
|
||||
return opt.SendToUsers.Contains(userId, StringComparer.OrdinalIgnoreCase);
|
||||
return opt.SendToUsers.Contains(userId, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace MediaBrowser.Model.Plugins
|
||||
NotSupported = -2,
|
||||
|
||||
/// <summary>
|
||||
/// This plugin caused an error when instantiated. (Either DI loop, or exception)
|
||||
/// This plugin caused an error when instantiated (either DI loop, or exception).
|
||||
/// </summary>
|
||||
Malfunctioned = -3,
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace MediaBrowser.Model.Providers
|
||||
/// <param name="key">Key for this id. This key should be unique across all providers.</param>
|
||||
/// <param name="type">Specific media type for this id.</param>
|
||||
/// <param name="urlFormatString">URL format string.</param>
|
||||
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string urlFormatString)
|
||||
public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
@@ -46,6 +46,6 @@ namespace MediaBrowser.Model.Providers
|
||||
/// <summary>
|
||||
/// Gets or sets the URL format string.
|
||||
/// </summary>
|
||||
public string UrlFormatString { get; set; }
|
||||
public string? UrlFormatString { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Querying
|
||||
{
|
||||
/// <summary>
|
||||
@@ -143,6 +145,7 @@ namespace MediaBrowser.Model.Querying
|
||||
/// <summary>
|
||||
/// The screenshot image tags.
|
||||
/// </summary>
|
||||
[Obsolete("Screenshot image type is no longer used.")]
|
||||
ScreenshotImageTags,
|
||||
|
||||
SeriesPrimaryImage,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Model.Querying
|
||||
@@ -48,7 +49,7 @@ namespace MediaBrowser.Model.Querying
|
||||
/// Gets or sets the include item types.
|
||||
/// </summary>
|
||||
/// <value>The include item types.</value>
|
||||
public string[] IncludeItemTypes { get; set; }
|
||||
public BaseItemKind[] IncludeItemTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is played.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Model.Search
|
||||
{
|
||||
@@ -16,8 +17,8 @@ namespace MediaBrowser.Model.Search
|
||||
IncludeStudios = true;
|
||||
|
||||
MediaTypes = Array.Empty<string>();
|
||||
IncludeItemTypes = Array.Empty<string>();
|
||||
ExcludeItemTypes = Array.Empty<string>();
|
||||
IncludeItemTypes = Array.Empty<BaseItemKind>();
|
||||
ExcludeItemTypes = Array.Empty<BaseItemKind>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -56,9 +57,9 @@ namespace MediaBrowser.Model.Search
|
||||
|
||||
public string[] MediaTypes { get; set; }
|
||||
|
||||
public string[] IncludeItemTypes { get; set; }
|
||||
public BaseItemKind[] IncludeItemTypes { get; set; }
|
||||
|
||||
public string[] ExcludeItemTypes { get; set; }
|
||||
public BaseItemKind[] ExcludeItemTypes { get; set; }
|
||||
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
#nullable disable
|
||||
namespace MediaBrowser.Model.Session
|
||||
{
|
||||
@@ -8,10 +10,9 @@ namespace MediaBrowser.Model.Session
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the item type.
|
||||
/// Artist, Genre, Studio, Person, or any kind of BaseItem.
|
||||
/// </summary>
|
||||
/// <value>The type of the item.</value>
|
||||
public string ItemType { get; set; }
|
||||
public BaseItemKind ItemType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the item id.
|
||||
|
||||
@@ -39,7 +39,6 @@ namespace MediaBrowser.Model.Session
|
||||
SetRepeatMode = 29,
|
||||
ChannelUp = 30,
|
||||
ChannelDown = 31,
|
||||
SetMaxStreamingBitrate = 31,
|
||||
Guide = 32,
|
||||
ToggleStats = 33,
|
||||
PlayMediaSource = 34,
|
||||
@@ -48,6 +47,7 @@ namespace MediaBrowser.Model.Session
|
||||
PlayState = 37,
|
||||
PlayNext = 38,
|
||||
ToggleOsdMenu = 39,
|
||||
Play = 40
|
||||
Play = 40,
|
||||
SetMaxStreamingBitrate = 41
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,42 +6,42 @@
|
||||
public enum HardwareEncodingType
|
||||
{
|
||||
/// <summary>
|
||||
/// AMD AMF
|
||||
/// AMD AMF.
|
||||
/// </summary>
|
||||
AMF = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Intel Quick Sync Video
|
||||
/// Intel Quick Sync Video.
|
||||
/// </summary>
|
||||
QSV = 1,
|
||||
|
||||
/// <summary>
|
||||
/// NVIDIA NVENC
|
||||
/// NVIDIA NVENC.
|
||||
/// </summary>
|
||||
NVENC = 2,
|
||||
|
||||
/// <summary>
|
||||
/// OpenMax OMX
|
||||
/// OpenMax OMX.
|
||||
/// </summary>
|
||||
OMX = 3,
|
||||
|
||||
/// <summary>
|
||||
/// Exynos V4L2 MFC
|
||||
/// Exynos V4L2 MFC.
|
||||
/// </summary>
|
||||
V4L2M2M = 4,
|
||||
|
||||
/// <summary>
|
||||
/// MediaCodec Android
|
||||
/// MediaCodec Android.
|
||||
/// </summary>
|
||||
MediaCodec = 5,
|
||||
|
||||
/// <summary>
|
||||
/// Video Acceleration API (VAAPI)
|
||||
/// Video Acceleration API (VAAPI).
|
||||
/// </summary>
|
||||
VAAPI = 6,
|
||||
|
||||
/// <summary>
|
||||
/// Video ToolBox
|
||||
/// Video ToolBox.
|
||||
/// </summary>
|
||||
VideoToolBox = 7
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace MediaBrowser.Model.Session
|
||||
VideoProfileNotSupported = 19,
|
||||
AudioBitDepthNotSupported = 20,
|
||||
SubtitleCodecNotSupported = 21,
|
||||
DirectPlayError = 22
|
||||
DirectPlayError = 22,
|
||||
AudioIsExternal = 23
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace MediaBrowser.Model.Tasks
|
||||
/// <param name="logger">The <see cref="ILogger"/>.</param>
|
||||
/// <param name="taskName">The name of the task.</param>
|
||||
/// <param name="isApplicationStartup">Wheter or not this is is fired during startup.</param>
|
||||
void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup);
|
||||
void Start(TaskResult? lastResult, ILogger logger, string taskName, bool isApplicationStartup);
|
||||
|
||||
/// <summary>
|
||||
/// Stops waiting for the trigger action.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Users
|
||||
{
|
||||
public class PinRedeemResult
|
||||
@@ -15,6 +16,6 @@ namespace MediaBrowser.Model.Users
|
||||
/// Gets or sets the users reset.
|
||||
/// </summary>
|
||||
/// <value>The users reset.</value>
|
||||
public string[] UsersReset { get; set; }
|
||||
public string[] UsersReset { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user