Merge branch 'master' into bug/authorization-header-issue

This commit is contained in:
Tommaso Stocchi
2021-06-03 17:15:32 +02:00
committed by GitHub
1151 changed files with 25088 additions and 14099 deletions

View File

@@ -33,7 +33,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath;
DataPath = Path.Combine(ProgramDataPath, "data");
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
}
/// <summary>
@@ -55,11 +55,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
public string DataPath
{
get => _dataPath;
private set => _dataPath = Directory.CreateDirectory(value).FullName;
}
public string DataPath => _dataPath;
/// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%";

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,6 +25,11 @@ namespace Emby.Server.Implementations.AppBase
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -31,11 +38,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
private bool _configurationLoaded;
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
/// <summary>
/// The _configuration.
/// </summary>

View File

@@ -1,9 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Serialization;
namespace Emby.Server.Implementations.AppBase
@@ -36,7 +33,8 @@ namespace Emby.Server.Implementations.AppBase
}
catch (Exception)
{
configuration = Activator.CreateInstance(type) ?? throw new ArgumentException($"Provided path ({type}) is not valid.", nameof(type));
// Note: CreateInstance returns null for Nullable<T>, e.g. CreateInstance(typeof(int?)) returns null.
configuration = Activator.CreateInstance(type)!;
}
using var stream = new MemoryStream(buffer?.Length ?? 0);
@@ -53,7 +51,8 @@ namespace Emby.Server.Implementations.AppBase
Directory.CreateDirectory(directory);
// Save it after load in case we got new items
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
{
fs.Write(newBytes, 0, newBytesLen);
}

View File

@@ -1,16 +1,17 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
@@ -42,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Session;
using Emby.Server.Implementations.SyncPlay;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Udp;
using Emby.Server.Implementations.Updates;
using Jellyfin.Api.Helpers;
using Jellyfin.Networking.Configuration;
@@ -97,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime;
@@ -116,10 +119,12 @@ namespace Emby.Server.Implementations
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
private string[] _urlPrefixes;
@@ -181,16 +186,6 @@ namespace Emby.Server.Implementations
protected IServiceCollection ServiceCollection { get; }
private IPlugin[] _plugins;
private IReadOnlyList<LocalPlugin> _pluginsManifests;
/// <summary>
/// Gets the plugins.
/// </summary>
/// <value>The plugins.</value>
public IReadOnlyList<IPlugin> Plugins => _plugins;
/// <summary>
/// Gets the logger factory.
/// </summary>
@@ -217,7 +212,7 @@ namespace Emby.Server.Implementations
/// Gets or sets the configuration manager.
/// </summary>
/// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; }
public ServerConfigurationManager ConfigurationManager { get; set; }
/// <summary>
/// Gets or sets the service provider.
@@ -235,10 +230,9 @@ namespace Emby.Server.Implementations
public int HttpsPort { get; private set; }
/// <summary>
/// Gets the server configuration manager.
/// Gets the value of the PublishedServerUrl setting.
/// </summary>
/// <value>The server configuration manager.</value>
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -246,54 +240,39 @@ namespace Emby.Server.Implementations
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
public ApplicationHost(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IStartupOptions options,
IConfiguration startupConfig,
IFileSystem fileSystem,
IServiceCollection serviceCollection)
{
_xmlSerializer = new MyXmlSerializer();
_jsonSerializer = new JsonSerializer();
ServiceCollection = serviceCollection;
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
_fileSystemManager = fileSystem;
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
// Have to pre-register the NetworkConfigurationFactory, as the configuration sub-system is not yet initialised.
ConfigurationManager.RegisterConfiguration<NetworkConfigurationFactory>();
NetManager = new NetworkManager((IServerConfigurationManager)ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
ServiceCollection = serviceCollection;
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_startupOptions = options;
// Initialize runtime stat collection
if (ServerConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
CertificateInfo = new CertificateInfo
{
Path = ServerConfigurationManager.Configuration.CertificatePath,
Password = ServerConfigurationManager.Configuration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
}
/// <summary>
@@ -306,9 +285,9 @@ namespace Emby.Server.Implementations
if (!File.Exists(path))
{
var networkSettings = new NetworkConfiguration();
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
_xmlSerializer.SerializeToFile(networkSettings, path);
Logger?.LogDebug("Successfully migrated network settings.");
Logger.LogDebug("Successfully migrated network settings.");
}
}
@@ -358,10 +337,7 @@ namespace Emby.Server.Implementations
{
get
{
if (_deviceId == null)
{
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
}
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
return _deviceId.Value;
}
@@ -381,7 +357,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Creates an instance of type and resolves all constructor dependencies.
/// </summary>
/// /// <typeparam name="T">The type.</typeparam>
/// <typeparam name="T">The type.</typeparam>
/// <returns>T.</returns>
public T CreateInstance<T>()
=> ActivatorUtilities.CreateInstance<T>(ServiceProvider);
@@ -393,16 +369,38 @@ namespace Emby.Server.Implementations
/// <returns>System.Object.</returns>
protected object CreateInstanceSafe(Type type)
{
_creatingInstances ??= new List<Type>();
if (_creatingInstances.IndexOf(type) != -1)
{
Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
foreach (var entry in _creatingInstances)
{
Logger.LogError("Called from: {TypeName}", entry.FullName);
}
_pluginManager.FailPlugin(type.Assembly);
throw new ExternalException("DI Loop detected.");
}
try
{
_creatingInstances.Add(type);
Logger.LogDebug("Creating instance of {Type}", type);
return ActivatorUtilities.CreateInstance(ServiceProvider, type);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating {Type}", type);
// If this is a plugin fail it.
_pluginManager.FailPlugin(type.Assembly);
return null;
}
finally
{
_creatingInstances.Remove(type);
}
}
/// <summary>
@@ -412,11 +410,7 @@ namespace Emby.Server.Implementations
/// <returns>``0.</returns>
public T Resolve<T>() => ServiceProvider.GetService<T>();
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
/// <inheritdoc/>
public IEnumerable<Type> GetExportTypes<T>()
{
var currentType = typeof(T);
@@ -445,17 +439,40 @@ namespace Emby.Server.Implementations
return parts;
}
/// <inheritdoc />
public IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true)
{
// Convert to list so this isn't executed for each iteration
var parts = GetExportTypes<T>()
.Select(i => defaultFunc(i))
.Where(i => i != null)
.Cast<T>()
.ToList();
if (manageLifetime)
{
lock (_disposableParts)
{
_disposableParts.AddRange(parts.OfType<IDisposable>());
}
}
return parts;
}
/// <summary>
/// Runs the startup tasks.
/// </summary>
/// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync()
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
_mediaEncoder.SetFFmpegPath();
@@ -463,14 +480,21 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch();
stopWatch.Start();
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
stopWatch.Stop();
@@ -494,7 +518,21 @@ namespace Emby.Server.Implementations
/// <inheritdoc/>
public void Init()
{
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
DiscoverTypes();
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
// Have to migrate settings here as migration subsystem not yet initialised.
MigrateNetworkConfiguration();
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics)
{
DotNetRuntimeStatsBuilder.Default().StartCollecting();
}
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
HttpPort = networkConfiguration.HttpServerPortNumber;
HttpsPort = networkConfiguration.HttpsPortNumber;
@@ -505,11 +543,16 @@ namespace Emby.Server.Implementations
HttpsPort = NetworkConfiguration.DefaultHttpsPort;
}
DiscoverTypes();
CertificateInfo = new CertificateInfo
{
Path = networkConfiguration.CertificatePath,
Password = networkConfiguration.CertificatePassword
};
Certificate = GetCertificate(CertificateInfo);
RegisterServices();
RegisterPluginServices();
_pluginManager.RegisterServices(ServiceCollection);
}
/// <summary>
@@ -521,13 +564,12 @@ namespace Emby.Server.Implementations
ServiceCollection.AddMemoryCache();
ServiceCollection.AddSingleton(ConfigurationManager);
ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
ServiceCollection.AddSingleton<IApplicationHost>(this);
ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
ServiceCollection.AddSingleton(_fileSystemManager);
ServiceCollection.AddSingleton<TmdbClientManager>();
@@ -550,8 +592,6 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
ServiceCollection.AddSingleton(ServerConfigurationManager);
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
@@ -563,12 +603,8 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -633,14 +669,14 @@ namespace Emby.Server.Implementations
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
ServiceCollection.AddSingleton<EncodingHelper>();
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
ServiceCollection.AddSingleton<TranscodingJobHelper>();
ServiceCollection.AddScoped<MediaInfoHelper>();
ServiceCollection.AddScoped<AudioHelper>();
ServiceCollection.AddScoped<DynamicHlsHelper>();
ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
}
/// <summary>
@@ -714,7 +750,7 @@ namespace Emby.Server.Implementations
// Don't use an empty string password
var password = string.IsNullOrWhiteSpace(info.Password) ? null : info.Password;
var localCert = new X509Certificate2(certificateLocation, password);
var localCert = new X509Certificate2(certificateLocation, password, X509KeyStorageFlags.UserKeySet);
// localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA;
if (!localCert.HasPrivateKey)
{
@@ -738,7 +774,7 @@ namespace Emby.Server.Implementations
{
// For now there's no real way to inject these properly
BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.ConfigurationManager = ServerConfigurationManager;
BaseItem.ConfigurationManager = ConfigurationManager;
BaseItem.LibraryManager = Resolve<ILibraryManager>();
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
@@ -752,7 +788,6 @@ namespace Emby.Server.Implementations
UserView.CollectionManager = Resolve<ICollectionManager>();
BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
CollectionFolder.XmlSerializer = _xmlSerializer;
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
}
@@ -761,41 +796,13 @@ namespace Emby.Server.Implementations
/// </summary>
private void FindParts()
{
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
if (!ConfigurationManager.Configuration.IsPortAuthorized)
{
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.Configuration.IsPortAuthorized = true;
ConfigurationManager.SaveConfiguration();
}
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
_plugins = GetExports<IPlugin>()
.Where(i => i != null)
.ToArray();
if (Plugins != null)
{
foreach (var plugin in Plugins)
{
if (_pluginsManifests != null && plugin is IPluginAssembly assemblyPlugin)
{
// Ensure the version number matches the Plugin Manifest information.
foreach (var item in _pluginsManifests)
{
if (Path.GetDirectoryName(plugin.AssemblyFilePath).Equals(item.Path, StringComparison.OrdinalIgnoreCase))
{
// Update version number to that of the manifest.
assemblyPlugin.SetAttributes(
plugin.AssemblyFilePath,
Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(plugin.AssemblyFilePath)),
item.Version);
break;
}
}
}
Logger.LogInformation("Loaded plugin: {PluginName} {PluginVersion}", plugin.Name, plugin.Version);
}
}
_pluginManager.CreatePlugins();
_urlPrefixes = GetUrlPrefixes().ToArray();
@@ -834,22 +841,6 @@ namespace Emby.Server.Implementations
_allConcreteTypes = GetTypes(GetComposablePartAssemblies()).ToArray();
}
private void RegisterPluginServices()
{
foreach (var pluginServiceRegistrator in GetExportTypes<IPluginServiceRegistrator>())
{
try
{
var instance = (IPluginServiceRegistrator)Activator.CreateInstance(pluginServiceRegistrator);
instance.RegisterServices(ServiceCollection);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error registering plugin services from {Assembly}.", pluginServiceRegistrator.Assembly);
}
}
}
private IEnumerable<Type> GetTypes(IEnumerable<Assembly> assemblies)
{
foreach (var ass in assemblies)
@@ -862,11 +853,13 @@ namespace Emby.Server.Implementations
catch (FileNotFoundException ex)
{
Logger.LogError(ex, "Error getting exported types from {Assembly}", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
catch (TypeLoadException ex)
{
Logger.LogError(ex, "Error loading types from {Assembly}.", ass.FullName);
_pluginManager.FailPlugin(ass);
continue;
}
@@ -912,19 +905,19 @@ namespace Emby.Server.Implementations
protected void OnConfigurationUpdated(object sender, EventArgs e)
{
var requiresRestart = false;
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
// Don't do anything if these haven't been set yet
if (HttpPort != 0 && HttpsPort != 0)
{
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
// Need to restart if ports have changed
if (networkConfiguration.HttpServerPortNumber != HttpPort ||
networkConfiguration.HttpsPortNumber != HttpsPort)
{
if (ServerConfigurationManager.Configuration.IsPortAuthorized)
if (ConfigurationManager.Configuration.IsPortAuthorized)
{
ServerConfigurationManager.Configuration.IsPortAuthorized = false;
ServerConfigurationManager.SaveConfiguration();
ConfigurationManager.Configuration.IsPortAuthorized = false;
ConfigurationManager.SaveConfiguration();
requiresRestart = true;
}
@@ -936,10 +929,7 @@ namespace Emby.Server.Implementations
requiresRestart = true;
}
var currentCertPath = CertificateInfo?.Path;
var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
if (ValidateSslCertificate(networkConfiguration))
{
requiresRestart = true;
}
@@ -952,6 +942,33 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="networkConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private bool ValidateSslCertificate(NetworkConfiguration networkConfig)
{
var newPath = networkConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(CertificateInfo?.Path, newPath, StringComparison.Ordinal))
{
if (File.Exists(newPath))
{
return true;
}
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
return false;
}
/// <summary>
/// Notifies that the kernel that a change has been made that requires a restart.
/// </summary>
@@ -1005,129 +1022,15 @@ namespace Emby.Server.Implementations
protected abstract void RestartInternal();
/// <inheritdoc/>
public IEnumerable<LocalPlugin> GetLocalPlugins(string path, bool cleanup = true)
{
var minimumVersion = new Version(0, 0, 0, 1);
var versions = new List<LocalPlugin>();
if (!Directory.Exists(path))
{
// Plugin path doesn't exist, don't try to enumerate subfolders.
return Enumerable.Empty<LocalPlugin>();
}
var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly);
foreach (var dir in directories)
{
try
{
var metafile = Path.Combine(dir, "meta.json");
if (File.Exists(metafile))
{
var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile);
if (!Version.TryParse(manifest.TargetAbi, out var targetAbi))
{
targetAbi = minimumVersion;
}
if (!Version.TryParse(manifest.Version, out var version))
{
version = minimumVersion;
}
if (ApplicationVersion >= targetAbi)
{
// Only load Plugins if the plugin is built for this version or below.
versions.Add(new LocalPlugin(manifest.Guid, manifest.Name, version, dir));
}
}
else
{
// No metafile, so lets see if the folder is versioned.
metafile = dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)[^1];
int versionIndex = dir.LastIndexOf('_');
if (versionIndex != -1 && Version.TryParse(dir.AsSpan()[(versionIndex + 1)..], out Version parsedVersion))
{
// Versioned folder.
versions.Add(new LocalPlugin(Guid.Empty, metafile, parsedVersion, dir));
}
else
{
// Un-versioned folder - Add it under the path name and version 0.0.0.1.
versions.Add(new LocalPlugin(Guid.Empty, metafile, minimumVersion, dir));
}
}
}
catch
{
continue;
}
}
string lastName = string.Empty;
versions.Sort(LocalPlugin.Compare);
// Traverse backwards through the list.
// The first item will be the latest version.
for (int x = versions.Count - 1; x >= 0; x--)
{
if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase))
{
versions[x].DllFiles.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories));
lastName = versions[x].Name;
continue;
}
if (!string.IsNullOrEmpty(lastName) && cleanup)
{
// Attempt a cleanup of old folders.
try
{
Logger.LogDebug("Deleting {Path}", versions[x].Path);
Directory.Delete(versions[x].Path, true);
}
catch (Exception e)
{
Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path);
}
versions.RemoveAt(x);
}
}
return versions;
}
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
/// <returns>IEnumerable{Assembly}.</returns>
protected IEnumerable<Assembly> GetComposablePartAssemblies()
{
if (Directory.Exists(ApplicationPaths.PluginsPath))
foreach (var p in _pluginManager.LoadAssemblies())
{
_pluginsManifests = GetLocalPlugins(ApplicationPaths.PluginsPath).ToList();
foreach (var plugin in _pluginsManifests)
{
foreach (var file in plugin.DllFiles)
{
Assembly plugAss;
try
{
plugAss = Assembly.LoadFrom(file);
}
catch (FileLoadException ex)
{
Logger.LogError(ex, "Failed to load assembly {Path}", file);
continue;
}
Logger.LogInformation("Loaded assembly {Assembly} from {Path}", plugAss.FullName, file);
yield return plugAss;
}
}
yield return p;
}
// Include composable parts in the Model assembly
@@ -1230,16 +1133,16 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
/// <inheritdoc/>
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(ipAddress, out port);
@@ -1256,10 +1159,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(HttpRequest request, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(request, out port);
@@ -1276,10 +1179,10 @@ namespace Emby.Server.Implementations
public string GetSmartApiUrl(string hostname, int? port = null)
{
// Published server ends with a /
if (_startupOptions.PublishedServerUrl != null)
if (!string.IsNullOrEmpty(PublishedServerUrl))
{
// Published server ends with a '/', so we need to remove it.
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
return PublishedServerUrl.Trim('/');
}
string smart = NetManager.GetBindInterface(hostname, out port);
@@ -1314,14 +1217,14 @@ namespace Emby.Server.Implementations
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
Host = host,
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
}.ToString().TrimEnd('/');
}
public string FriendlyName =>
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
? Environment.MachineName
: ServerConfigurationManager.Configuration.ServerName;
: ConfigurationManager.Configuration.ServerName;
/// <summary>
/// Shuts down.
@@ -1369,17 +1272,6 @@ namespace Emby.Server.Implementations
}
}
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void RemovePlugin(IPlugin plugin)
{
var list = _plugins.ToList();
list.Remove(plugin);
_plugins = list.ToArray();
}
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes

View File

@@ -1,13 +1,17 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@@ -21,7 +25,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,10 +47,10 @@ namespace Emby.Server.Implementations.Channels
private readonly ILogger<ChannelManager> _logger;
private readonly IServerConfigurationManager _config;
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
/// <summary>
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
@@ -59,7 +62,6 @@ namespace Emby.Server.Implementations.Channels
/// <param name="config">The server configuration manager.</param>
/// <param name="fileSystem">The filesystem.</param>
/// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
@@ -70,7 +72,6 @@ namespace Emby.Server.Implementations.Channels
IServerConfigurationManager config,
IFileSystem fileSystem,
IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
IProviderManager providerManager,
IMemoryCache memoryCache)
{
@@ -81,7 +82,6 @@ namespace Emby.Server.Implementations.Channels
_config = config;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
_memoryCache = memoryCache;
}
@@ -337,21 +337,23 @@ namespace Emby.Server.Implementations.Channels
return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
}
private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
try
{
return _jsonSerializer.DeserializeFromFile<List<MediaSourceInfo>>(path) ?? new List<MediaSourceInfo>();
var bytes = File.ReadAllBytes(path);
return JsonSerializer.Deserialize<MediaSourceInfo[]>(bytes, _jsonOptions)
?? Array.Empty<MediaSourceInfo>();
}
catch
{
return new List<MediaSourceInfo>();
return Array.Empty<MediaSourceInfo>();
}
}
private void SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
private async Task SaveMediaSources(BaseItem item, List<MediaSourceInfo> mediaSources)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasourceinfos.json");
@@ -370,7 +372,8 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(mediaSources, path);
await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
}
/// <inheritdoc />
@@ -812,7 +815,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -834,7 +838,8 @@ namespace Emby.Server.Implementations.Channels
{
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{
var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath);
await using FileStream jsonStream = File.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (cachedResult != null)
{
return null;
@@ -865,7 +870,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
}
CacheResponse(result, cachePath);
await CacheResponse(result, cachePath);
return result;
}
@@ -875,13 +880,14 @@ namespace Emby.Server.Implementations.Channels
}
}
private void CacheResponse(object result, string path)
private async Task CacheResponse(object result, string path)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path));
_jsonSerializer.SerializeToFile(result, path);
await using FileStream createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1176,11 +1182,11 @@ namespace Emby.Server.Implementations.Channels
{
if (enableMediaProbe && !info.IsLiveStream && item.HasPathProtocol)
{
SaveMediaSources(item, new List<MediaSourceInfo>());
await SaveMediaSources(item, new List<MediaSourceInfo>()).ConfigureAwait(false);
}
else
{
SaveMediaSources(item, info.MediaSources);
await SaveMediaSources(item, info.MediaSources).ConfigureAwait(false);
}
}

View File

@@ -82,9 +82,9 @@ namespace Emby.Server.Implementations.Collections
return null;
})
.Where(i => i != null)
.GroupBy(x => x.Id)
.GroupBy(x => x!.Id) // We removed the null values
.Select(x => x.First())
.ToList();
.ToList()!; // Again... the list doesn't contain any null values
}
/// <inheritdoc />

View File

@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
@@ -8,11 +9,9 @@ using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
@@ -107,7 +106,7 @@ namespace Emby.Server.Implementations.Collections
var name = _localizationManager.GetLocalizedString("Collections");
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
return FindFolders(path).First();
}
@@ -124,7 +123,7 @@ namespace Emby.Server.Implementations.Collections
private IEnumerable<BoxSet> GetCollections(User user)
{
var folder = GetCollectionsFolder(false).Result;
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
return folder == null
? Enumerable.Empty<BoxSet>()
@@ -167,7 +166,7 @@ namespace Emby.Server.Implementations.Collections
parentFolder.AddChild(collection, CancellationToken.None);
if (options.ItemIdList.Length > 0)
if (options.ItemIdList.Count > 0)
{
await AddToCollectionAsync(
collection.Id,
@@ -251,11 +250,7 @@ namespace Emby.Server.Implementations.Collections
if (fireEvent)
{
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
{
Collection = collection,
ItemsChanged = itemList
});
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
}
}
@@ -307,11 +302,7 @@ namespace Emby.Server.Implementations.Collections
},
RefreshPriority.High);
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
{
Collection = collection,
ItemsChanged = itemList
});
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
}
/// <inheritdoc />
@@ -319,11 +310,11 @@ namespace Emby.Server.Implementations.Collections
{
var results = new Dictionary<Guid, BaseItem>();
var allBoxsets = GetCollections(user).ToList();
var allBoxSets = GetCollections(user).ToList();
foreach (var item in items)
{
if (!(item is ISupportsBoxSetGrouping))
if (item is not ISupportsBoxSetGrouping)
{
results[item.Id] = item;
}
@@ -331,20 +322,44 @@ namespace Emby.Server.Implementations.Collections
{
var itemId = item.Id;
var currentBoxSets = allBoxsets
.Where(i => i.ContainsLinkedChildByItemId(itemId))
.ToList();
if (currentBoxSets.Count > 0)
var itemIsInBoxSet = false;
foreach (var boxSet in allBoxSets)
{
foreach (var boxset in currentBoxSets)
if (!boxSet.ContainsLinkedChildByItemId(itemId))
{
results[boxset.Id] = boxset;
continue;
}
itemIsInBoxSet = true;
results.TryAdd(boxSet.Id, boxSet);
}
// skip any item that is in a box set
if (itemIsInBoxSet)
{
continue;
}
var alreadyInResults = false;
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
if (item is Video video)
{
foreach (var childId in video.GetLocalAlternateVersionIds())
{
if (!results.ContainsKey(childId))
{
continue;
}
alreadyInResults = true;
break;
}
}
else
if (!alreadyInResults)
{
results[item.Id] = item;
results[itemId] = item;
}
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Globalization;
using System.IO;
@@ -88,38 +90,12 @@ namespace Emby.Server.Implementations.Configuration
var newConfig = (ServerConfiguration)newConfiguration;
ValidateMetadataPath(newConfig);
ValidateSslCertificate(newConfig);
ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig));
base.ReplaceConfiguration(newConfiguration);
}
/// <summary>
/// Validates the SSL certificate.
/// </summary>
/// <param name="newConfig">The new configuration.</param>
/// <exception cref="FileNotFoundException">The certificate path doesn't exist.</exception>
private void ValidateSslCertificate(BaseApplicationConfiguration newConfig)
{
var serverConfig = (ServerConfiguration)newConfig;
var newPath = serverConfig.CertificatePath;
if (!string.IsNullOrWhiteSpace(newPath)
&& !string.Equals(Configuration.CertificatePath, newPath, StringComparison.Ordinal))
{
if (!File.Exists(newPath))
{
throw new FileNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"Certificate file '{0}' does not exist.",
newPath));
}
}
}
/// <summary>
/// Validates the metadata path.
/// </summary>

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations

View File

@@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Security.Cryptography;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -181,11 +183,9 @@ namespace Emby.Server.Implementations.Data
foreach (var row in connection.Query("PRAGMA table_info(" + table + ")"))
{
if (row[1].SQLiteType != SQLiteType.Null)
if (row.TryGetString(1, out var columnName))
{
var name = row[1].ToString();
columnNames.Add(name);
columnNames.Add(columnName);
}
}

View File

@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations.Data
{
public class ManagedConnection : IDisposable
{
private SQLiteDatabaseConnection _db;
private SQLiteDatabaseConnection? _db;
private readonly SemaphoreSlim _writeLock;
private bool _disposed = false;
@@ -54,12 +54,12 @@ namespace Emby.Server.Implementations.Data
return _db.RunInTransaction(action, mode);
}
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql)
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
{
return _db.Query(sql);
}
public IEnumerable<IReadOnlyList<IResultSetValue>> Query(string sql, params object[] values)
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
{
return _db.Query(sql, values);
}

View File

@@ -1,7 +1,9 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using SQLitePCL.pretty;
@@ -59,11 +61,11 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(conn =>
{
conn.ExecuteAll(string.Join(";", queries));
conn.ExecuteAll(string.Join(';', queries));
});
}
public static Guid ReadGuidFromBlob(this IResultSetValue result)
public static Guid ReadGuidFromBlob(this ResultSetValue result)
{
return new Guid(result.ToBlob());
}
@@ -84,7 +86,7 @@ namespace Emby.Server.Implementations.Data
private static string GetDateTimeKindFormat(DateTimeKind kind)
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
public static DateTime ReadDateTime(this IResultSetValue result)
public static DateTime ReadDateTime(this ResultSetValue result)
{
var dateText = result.ToString();
@@ -95,58 +97,147 @@ namespace Emby.Server.Implementations.Data
DateTimeStyles.None).ToUniversalTime();
}
public static DateTime? TryReadDateTime(this IResultSetValue result)
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
{
var dateText = result.ToString();
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
var dateText = item.ToString();
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out var dateTimeResult))
{
return dateTimeResult.ToUniversalTime();
result = dateTimeResult.ToUniversalTime();
return true;
}
return null;
result = default;
return false;
}
public static bool IsDBNull(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
{
return result[index].SQLiteType == SQLiteType.Null;
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ReadGuidFromBlob();
return true;
}
public static string GetString(this IReadOnlyList<IResultSetValue> result, int index)
public static bool IsDbNull(this ResultSetValue result)
{
return result.SQLiteType == SQLiteType.Null;
}
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToString();
}
public static bool GetBoolean(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
{
result = null;
var item = reader[index];
if (item.IsDbNull())
{
return false;
}
result = item.ToString();
return true;
}
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToBool();
}
public static int GetInt32(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
{
return result[index].ToInt();
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ToBool();
return true;
}
public static long GetInt64(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
{
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ToInt();
return true;
}
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ToInt64();
}
public static float GetFloat(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
{
return result[index].ToFloat();
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ToInt64();
return true;
}
public static Guid GetGuid(this IReadOnlyList<IResultSetValue> result, int index)
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
{
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ToFloat();
return true;
}
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
{
var item = reader[index];
if (item.IsDbNull())
{
result = default;
return false;
}
result = item.ToDouble();
return true;
}
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
{
return result[index].ReadGuidFromBlob();
}
[Conditional("DEBUG")]
private static void CheckName(string name)
{
#if DEBUG
throw new ArgumentException("Invalid param name: " + name, nameof(name));
#endif
}
public static void TryBind(this IStatement statement, string name, double value)
@@ -350,7 +441,7 @@ namespace Emby.Server.Implementations.Data
}
}
public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
{
while (statement.MoveNext())
{

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Data
connection.RunInTransaction(
db =>
{
db.ExecuteAll(string.Join(";", new[] {
db.ExecuteAll(string.Join(';', new[] {
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
@@ -348,16 +350,16 @@ namespace Emby.Server.Implementations.Data
/// Read a row from the specified reader into the provided userData object.
/// </summary>
/// <param name="reader"></param>
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
{
var userData = new UserItemData();
userData.Key = reader[0].ToString();
// userData.UserId = reader[1].ReadGuidFromBlob();
if (reader[2].SQLiteType != SQLiteType.Null)
if (reader.TryGetDouble(2, out var rating))
{
userData.Rating = reader[2].ToDouble();
userData.Rating = rating;
}
userData.Played = reader[3].ToBool();
@@ -365,19 +367,19 @@ namespace Emby.Server.Implementations.Data
userData.IsFavorite = reader[5].ToBool();
userData.PlaybackPositionTicks = reader[6].ToInt64();
if (reader[7].SQLiteType != SQLiteType.Null)
if (reader.TryReadDateTime(7, out var lastPlayedDate))
{
userData.LastPlayedDate = reader[7].TryReadDateTime();
userData.LastPlayedDate = lastPlayedDate;
}
if (reader[8].SQLiteType != SQLiteType.Null)
if (reader.TryGetInt32(8, out var audioStreamIndex))
{
userData.AudioStreamIndex = reader[8].ToInt();
userData.AudioStreamIndex = audioStreamIndex;
}
if (reader[9].SQLiteType != SQLiteType.Null)
if (reader.TryGetInt32(9, out var subtitleStreamIndex))
{
userData.SubtitleStreamIndex = reader[9].ToInt();
userData.SubtitleStreamIndex = subtitleStreamIndex;
}
return userData;

View File

@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Data
/// This holds all the types in the running assemblies
/// so that we can de-serialize properly when we don't have strong types.
/// </summary>
private readonly ConcurrentDictionary<string, Type> _typeMap = new ConcurrentDictionary<string, Type>();
private readonly ConcurrentDictionary<string, Type?> _typeMap = new ConcurrentDictionary<string, Type?>();
/// <summary>
/// Gets the type.
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.Data
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
/// <exception cref="ArgumentNullException"><c>typeName</c> is null.</exception>
public Type GetType(string typeName)
public Type? GetType(string typeName)
{
if (string.IsNullOrEmpty(typeName))
{
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Data
/// </summary>
/// <param name="typeName">Name of the type.</param>
/// <returns>Type.</returns>
private Type LookupType(string typeName)
private Type? LookupType(string typeName)
{
return AppDomain.CurrentDomain.GetAssemblies()
.Select(a => a.GetType(typeName))

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -249,7 +251,7 @@ namespace Emby.Server.Implementations.Dto
var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
if (activeRecording != null)
{
dto.Type = "Recording";
dto.Type = BaseItemKind.Recording;
dto.CanDownload = false;
dto.RunTimeTicks = null;
@@ -582,7 +584,26 @@ namespace Emby.Server.Implementations.Dto
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
if (dto.ImageBlurHashes != null)
{
// Only add BlurHash for the person's image.
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
{
if (blurHash != null)
{
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
foreach (var (imageId, blurHashValue) in blurHash)
{
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
{
baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
}
}
}
}
}
list.Add(baseItemPerson);
}
}
@@ -646,10 +667,7 @@ namespace Emby.Server.Implementations.Dto
var tag = GetImageCacheTag(item, image);
if (!string.IsNullOrEmpty(image.BlurHash))
{
if (dto.ImageBlurHashes == null)
{
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
}
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
if (!dto.ImageBlurHashes.ContainsKey(image.Type))
{
@@ -683,10 +701,7 @@ namespace Emby.Server.Implementations.Dto
if (hashes.Count > 0)
{
if (dto.ImageBlurHashes == null)
{
dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
}
dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
dto.ImageBlurHashes[imageType] = hashes;
}
@@ -879,13 +894,10 @@ namespace Emby.Server.Implementations.Dto
dto.Taglines = new string[] { item.Tagline };
}
if (dto.Taglines == null)
{
dto.Taglines = Array.Empty<string>();
}
dto.Taglines ??= Array.Empty<string>();
}
dto.Type = item.GetClientTypeName();
dto.Type = item.GetBaseItemKind();
if ((item.CommunityRating ?? 0) > 0)
{
dto.CommunityRating = item.CommunityRating;
@@ -1138,7 +1150,7 @@ namespace Emby.Server.Implementations.Dto
if (episodeSeries != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, episodeSeries);
}
@@ -1188,7 +1200,7 @@ namespace Emby.Server.Implementations.Dto
if (series != null)
{
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
{
AttachPrimaryImageAspectRatio(dto, series);
}

View File

@@ -9,6 +9,7 @@
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
@@ -23,24 +24,16 @@
<ItemGroup>
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="5.0.6" />
<PackageReference Include="Mono.Nat" Version="3.0.1" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.10.2" />
<PackageReference Include="sharpcompress" Version="0.26.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.1.0" />
<PackageReference Include="sharpcompress" Version="0.28.2" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.0.1" />
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
</ItemGroup>
<ItemGroup>
@@ -52,27 +45,24 @@
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
<AnalysisMode Condition=" '$(Configuration)' == 'Debug' ">AllEnabledByDefault</AnalysisMode>
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Localization\iso6392.txt" />
<EmbeddedResource Include="Localization\countries.json" />
<EmbeddedResource Include="Localization\Core\*.json" />
<EmbeddedResource Include="Localization\Ratings\*.csv" />
</ItemGroup>
</Project>

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -106,8 +108,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.StartDiscovery();
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
}
private void Stop()
@@ -118,13 +118,6 @@ namespace Emby.Server.Implementations.EntryPoints
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
_timer?.Dispose();
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
}
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
{
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,4 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
@@ -29,7 +30,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// <summary>
/// The UDP server.
/// </summary>
private UdpServer _udpServer;
private UdpServer? _udpServer;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false;
@@ -49,10 +50,12 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
CheckDisposed();
try
{
_udpServer = new UdpServer(_logger, _appHost, _config);
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
_udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
_udpServer.Start(_cancellationTokenSource.Token);
}
catch (SocketException ex)
{
@@ -62,6 +65,14 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(this.GetType().Name);
}
}
/// <inheritdoc />
public void Dispose()
{
@@ -71,9 +82,8 @@ namespace Emby.Server.Implementations.EntryPoints
}
_cancellationTokenSource.Cancel();
_udpServer.Dispose();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = null;
_udpServer?.Dispose();
_udpServer = null;
_disposed = true;

View File

@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
private readonly object _syncLock = new object();
private Timer _updateTimer;
private Timer? _updateTimer;
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
{
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.EntryPoints
return Task.CompletedTask;
}
void OnUserDataManagerUserDataSaved(object sender, UserDataSaveEventArgs e)
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
{
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
{
@@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.EntryPoints
_updateTimer.Change(UpdateDuration, Timeout.Infinite);
}
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem> keys))
if (!_changedItems.TryGetValue(e.UserId, out List<BaseItem>? keys))
{
keys = new List<BaseItem>();
_changedItems[e.UserId] = keys;
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
private void UpdateTimerCallback(object state)
private void UpdateTimerCallback(object? state)
{
lock (_syncLock)
{

View File

@@ -1,6 +1,5 @@
#pragma warning disable CS1591
using System;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Net;

View File

@@ -2,8 +2,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@@ -27,7 +27,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
{
return (AuthorizationInfo)cached;
return (AuthorizationInfo)cached!; // Cache should never contain null
}
return GetAuthorization(requestContext);
@@ -55,15 +55,15 @@ namespace Emby.Server.Implementations.HttpServer.Security
}
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in Dictionary<string, string>? auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
string deviceId = null;
string device = null;
string client = null;
string version = null;
string token = null;
string? deviceId = null;
string? device = null;
string? client = null;
string? version = null;
string? token = null;
if (auth != null)
{
@@ -206,7 +206,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
@@ -215,7 +215,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -223,7 +223,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
private Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
@@ -232,7 +232,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
return GetAuthorization(auth.Count > 0 ? auth[0] : null);
}
/// <summary>
@@ -240,25 +240,25 @@ namespace Emby.Server.Implementations.HttpServer.Security
/// </summary>
/// <param name="authorizationHeader">The authorization header.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
private Dictionary<string, string>? GetAuthorization(ReadOnlySpan<char> authorizationHeader)
{
if (authorizationHeader == null)
{
return null;
}
var parts = authorizationHeader.Split(' ', 2);
var firstSpace = authorizationHeader.IndexOf(' ');
// There should be at least to parts
if (parts.Length != 2)
// There should be at least two parts
if (firstSpace == -1)
{
return null;
}
var acceptedNames = new[] { "MediaBrowser", "Emby" };
var name = authorizationHeader[..firstSpace];
// It has to be a digest request
if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
if (!name.Equals("MediaBrowser", StringComparison.OrdinalIgnoreCase)
&& !name.Equals("Emby", StringComparison.OrdinalIgnoreCase))
{
return null;
}

View File

@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
var authorization = _authContext.GetAuthorizationInfo(requestContext);
var user = authorization.User;
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp().ToString(), user);
}
public SessionInfo GetSession(object requestContext)
@@ -36,16 +36,16 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetSession((HttpContext)requestContext);
}
public User GetUser(HttpContext requestContext)
public User? GetUser(HttpContext requestContext)
{
var session = GetSession(requestContext);
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
}
public User GetUser(object requestContext)
public User? GetUser(object requestContext)
{
return GetUser((HttpContext)requestContext);
return GetUser(((HttpRequest)requestContext).HttpContext);
}
}
}

View File

@@ -1,10 +1,9 @@
#nullable enable
using System;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
RemoteEndPoint = remoteEndPoint;
QueryString = query;
_jsonOptions = JsonDefaults.GetOptions();
_jsonOptions = JsonDefaults.Options;
LastActivityDate = DateTime.Now;
}
@@ -138,7 +137,7 @@ namespace Emby.Server.Implementations.HttpServer
writer.Advance(bytesRead);
// Make the data available to the PipeReader
FlushResult flushResult = await writer.FlushAsync().ConfigureAwait(false);
FlushResult flushResult = await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
if (flushResult.IsCompleted)
{
// The PipeReader stopped reading
@@ -181,32 +180,16 @@ namespace Emby.Server.Implementations.HttpServer
}
WebSocketMessage<object>? stub;
long bytesConsumed = 0;
try
{
if (buffer.IsSingleSegment)
{
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
}
else
{
var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
try
{
buffer.CopyTo(buf);
stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
}
finally
{
ArrayPool<byte>.Shared.Return(buf);
}
}
stub = DeserializeWebSocketMessage(buffer, out bytesConsumed);
}
catch (JsonException ex)
{
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
_logger.LogError(ex, "Error processing web socket message");
_logger.LogError(ex, "Error processing web socket message: {Data}", Encoding.UTF8.GetString(buffer));
return;
}
@@ -217,27 +200,34 @@ namespace Emby.Server.Implementations.HttpServer
}
// Tell the PipeReader how much of the buffer we have consumed
reader.AdvanceTo(buffer.End);
reader.AdvanceTo(buffer.GetPosition(bytesConsumed));
_logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
var info = new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(), // Data can be null
Connection = this
};
if (info.MessageType == SessionMessageType.KeepAlive)
if (stub.MessageType == SessionMessageType.KeepAlive)
{
await SendKeepAliveResponse().ConfigureAwait(false);
}
else
{
await OnReceive(info).ConfigureAwait(false);
await OnReceive(
new WebSocketMessageInfo
{
MessageType = stub.MessageType,
Data = stub.Data?.ToString(), // Data can be null
Connection = this
}).ConfigureAwait(false);
}
}
internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
{
var jsonReader = new Utf8JsonReader(bytes);
var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
bytesConsumed = jsonReader.BytesConsumed;
return ret;
}
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -14,15 +16,18 @@ namespace Emby.Server.Implementations.HttpServer
public class WebSocketManager : IWebSocketManager
{
private readonly IWebSocketListener[] _webSocketListeners;
private readonly IAuthService _authService;
private readonly ILogger<WebSocketManager> _logger;
private readonly ILoggerFactory _loggerFactory;
public WebSocketManager(
IAuthService authService,
IEnumerable<IWebSocketListener> webSocketListeners,
ILogger<WebSocketManager> logger,
ILoggerFactory loggerFactory)
{
_webSocketListeners = webSocketListeners.ToArray();
_authService = authService;
_logger = logger;
_loggerFactory = loggerFactory;
}
@@ -30,6 +35,7 @@ namespace Emby.Server.Implementations.HttpServer
/// <inheritdoc />
public async Task WebSocketRequestHandler(HttpContext context)
{
_ = _authService.Authenticate(context.Request);
try
{
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -2,11 +2,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.System;
@@ -24,7 +23,7 @@ namespace Emby.Server.Implementations.IO
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
private readonly string _tempPath;
private readonly bool _isEnvironmentCaseInsensitive;
private static readonly bool _isEnvironmentCaseInsensitive = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
@@ -32,8 +31,6 @@ namespace Emby.Server.Implementations.IO
{
Logger = logger;
_tempPath = applicationPaths.TempDirectory;
_isEnvironmentCaseInsensitive = OperatingSystem.Id == OperatingSystemId.Windows;
}
public virtual void AddShortcutHandler(IShortcutHandler handler)
@@ -64,7 +61,7 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">filename</exception>
public virtual string ResolveShortcut(string filename)
public virtual string? ResolveShortcut(string filename)
{
if (string.IsNullOrEmpty(filename))
{
@@ -72,7 +69,7 @@ namespace Emby.Server.Implementations.IO
}
var extension = Path.GetExtension(filename);
var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
var handler = _shortcutHandlers.Find(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase));
return handler?.Resolve(filename);
}
@@ -249,13 +246,20 @@ namespace Emby.Server.Implementations.IO
// Issue #2354 get the size of files behind symbolic links
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
try
{
result.Length = thisFileStream.Length;
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
{
result.Length = thisFileStream.Length;
}
}
catch (FileNotFoundException ex)
{
// Dangling symlinks cannot be detected before opening the file unfortunately...
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
result.Exists = false;
}
}
result.DirectoryName = fileInfo.DirectoryName;
}
result.CreationTimeUtc = GetCreationTimeUtc(info);
@@ -294,16 +298,37 @@ namespace Emby.Server.Implementations.IO
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
/// <exception cref="ArgumentNullException">The filename is null.</exception>
public virtual string GetValidFilename(string filename)
public string GetValidFilename(string filename)
{
var builder = new StringBuilder(filename);
foreach (var c in Path.GetInvalidFileNameChars())
var invalid = Path.GetInvalidFileNameChars();
var first = filename.IndexOfAny(invalid);
if (first == -1)
{
builder = builder.Replace(c, ' ');
// Fast path for clean strings
return filename;
}
return builder.ToString();
return string.Create(
filename.Length,
(filename, invalid, first),
(chars, state) =>
{
state.filename.AsSpan().CopyTo(chars);
chars[state.first++] = ' ';
var len = chars.Length;
foreach (var c in state.invalid)
{
for (int i = state.first; i < len; i++)
{
if (chars[i] == c)
{
chars[i] = ' ';
}
}
}
});
}
/// <summary>
@@ -487,26 +512,9 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path));
}
var separatorChar = Path.DirectorySeparatorChar;
return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1;
}
public virtual bool IsRootPath(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
var parent = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(parent))
{
return false;
}
return true;
return path.Contains(
Path.TrimEndingDirectorySeparator(parentPath) + Path.DirectorySeparatorChar,
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string NormalizePath(string path)
@@ -521,7 +529,7 @@ namespace Emby.Server.Implementations.IO
return path;
}
return path.TrimEnd(Path.DirectorySeparatorChar);
return Path.TrimEndingDirectorySeparator(path);
}
public virtual bool AreEqual(string path1, string path2)
@@ -536,7 +544,10 @@ namespace Emby.Server.Implementations.IO
return false;
}
return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase);
return string.Equals(
NormalizePath(path1),
NormalizePath(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
@@ -582,9 +593,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
@@ -592,18 +601,18 @@ namespace Emby.Server.Implementations.IO
return GetFiles(path, null, false, recursive);
}
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
{
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
}
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
if (extensions != null && extensions.Count > 0)
{
@@ -625,10 +634,10 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
{
var directoryInfo = new DirectoryInfo(path);
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var enumerationOptions = GetEnumerationOptions(recursive);
return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
}
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
@@ -638,8 +647,7 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return Directory.EnumerateDirectories(path, "*", searchOption);
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
}
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
@@ -647,18 +655,18 @@ namespace Emby.Server.Implementations.IO
return GetFilePaths(path, null, false, recursive);
}
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
var enumerationOptions = GetEnumerationOptions(recursive);
// On linux and osx the search pattern is case sensitive
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
{
return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
}
var files = Directory.EnumerateFiles(path, "*", searchOption);
var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
if (extensions != null && extensions.Length > 0)
{
@@ -679,23 +687,18 @@ namespace Emby.Server.Implementations.IO
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
{
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
}
private static void RunProcess(string path, string args, string workingDirectory)
private EnumerationOptions GetEnumerationOptions(bool recursive)
{
using (var process = Process.Start(new ProcessStartInfo
return new EnumerationOptions
{
Arguments = args,
FileName = path,
CreateNoWindow = true,
WorkingDirectory = workingDirectory,
WindowStyle = ProcessWindowStyle.Normal
}))
{
process.WaitForExit();
}
RecurseSubdirectories = recursive,
IgnoreInaccessible = true,
// Don't skip any files.
AttributesToSkip = 0
};
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.IO
public string Extension => ".mblink";
public string Resolve(string shortcutPath)
public string? Resolve(string shortcutPath)
{
if (string.IsNullOrEmpty(shortcutPath))
{

View File

@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.IO
{
public class StreamHelper : IStreamHelper
{
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken)
public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action? onStarted, CancellationToken cancellationToken)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try

View File

@@ -1,7 +1,5 @@
#pragma warning disable CS1591
using System;
namespace Emby.Server.Implementations
{
public interface IStartupOptions
@@ -9,7 +7,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --ffmpeg command line option.
/// </summary>
string FFmpegPath { get; }
string? FFmpegPath { get; }
/// <summary>
/// Gets the value of the --service command line option.
@@ -19,21 +17,21 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets the value of the --package-name command line option.
/// </summary>
string PackageName { get; }
string? PackageName { get; }
/// <summary>
/// Gets the value of the --restartpath command line option.
/// </summary>
string RestartPath { get; }
string? RestartPath { get; }
/// <summary>
/// Gets the value of the --restartargs command line option.
/// </summary>
string RestartArgs { get; }
string? RestartArgs { get; }
/// <summary>
/// Gets the value of the --published-server-url command line option.
/// </summary>
Uri PublishedServerUrl { get; }
string? PublishedServerUrl { get; }
}
}

View File

@@ -2,20 +2,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Images
{

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -191,7 +193,7 @@ namespace Emby.Server.Implementations.Images
InputPaths = GetStripCollageImagePaths(primaryItem, items).ToArray()
};
if (options.InputPaths.Length == 0)
if (options.InputPaths.Count == 0)
{
return null;
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,10 +1,11 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Emby.Server.Implementations.Images;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System.Collections.Generic;
@@ -29,9 +31,7 @@ namespace Emby.Server.Implementations.Images
{
var subItem = i.Item2;
var episode = subItem as Episode;
if (episode != null)
if (subItem is Episode episode)
{
var series = episode.Series;
if (series != null && series.HasImage(ImageType.Primary))

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Linq;
using DotNet.Globbing;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -48,6 +50,7 @@ using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
using VideoResolver = Emby.Naming.Video.VideoResolver;
@@ -175,10 +178,7 @@ namespace Emby.Server.Implementations.Library
{
lock (_rootFolderSyncLock)
{
if (_rootFolder == null)
{
_rootFolder = CreateRootFolder();
}
_rootFolder ??= CreateRootFolder();
}
}
@@ -196,33 +196,33 @@ namespace Emby.Server.Implementations.Library
/// Gets or sets the postscan tasks.
/// </summary>
/// <value>The postscan tasks.</value>
private ILibraryPostScanTask[] PostscanTasks { get; set; }
private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty<ILibraryPostScanTask>();
/// <summary>
/// Gets or sets the intro providers.
/// </summary>
/// <value>The intro providers.</value>
private IIntroProvider[] IntroProviders { get; set; }
private IIntroProvider[] IntroProviders { get; set; } = Array.Empty<IIntroProvider>();
/// <summary>
/// Gets or sets the list of entity resolution ignore rules.
/// </summary>
/// <value>The entity resolution ignore rules.</value>
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; }
private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty<IResolverIgnoreRule>();
/// <summary>
/// Gets or sets the list of currently registered entity resolvers.
/// </summary>
/// <value>The entity resolvers enumerable.</value>
private IItemResolver[] EntityResolvers { get; set; }
private IItemResolver[] EntityResolvers { get; set; } = Array.Empty<IItemResolver>();
private IMultiItemResolver[] MultiItemResolvers { get; set; }
private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty<IMultiItemResolver>();
/// <summary>
/// Gets or sets the comparers.
/// </summary>
/// <value>The comparers.</value>
private IBaseItemComparer[] Comparers { get; set; }
private IBaseItemComparer[] Comparers { get; set; } = Array.Empty<IBaseItemComparer>();
public bool IsScanRunning { get; private set; }
@@ -558,7 +558,6 @@ namespace Emby.Server.Implementations.Library
var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
{
Parent = parent,
Path = fullPath,
FileInfo = fileInfo,
CollectionType = collectionType,
LibraryOptions = libraryOptions
@@ -684,7 +683,7 @@ namespace Emby.Server.Implementations.Library
foreach (var item in items)
{
ResolverHelper.SetInitialItemValues(item, parent, _fileSystem, this, directoryService);
ResolverHelper.SetInitialItemValues(item, parent, this, directoryService);
}
items.AddRange(ResolveFileList(result.ExtraFiles, directoryService, parent, collectionType, resolvers, libraryOptions));
@@ -1163,7 +1162,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
_itemRepository.UpdateInheritedValues(cancellationToken);
_itemRepository.UpdateInheritedValues();
progress.Report(100);
}
@@ -1240,11 +1239,20 @@ namespace Emby.Server.Implementations.Library
return info;
}
private string GetCollectionType(string path)
private CollectionTypeOptions? GetCollectionType(string path)
{
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
.Select(Path.GetFileNameWithoutExtension)
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
foreach (var file in files)
{
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
// https://github.com/dotnet/runtime/issues/20008
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
{
return res;
}
}
return null;
}
/// <summary>
@@ -1905,12 +1913,17 @@ namespace Emby.Server.Implementations.Library
}
catch (ArgumentException)
{
_logger.LogWarning("Cannot get image index for {0}", img.Path);
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
continue;
}
catch (InvalidOperationException)
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
{
_logger.LogWarning("Cannot fetch image from {0}", img.Path);
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
continue;
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
continue;
}
}
@@ -1923,7 +1936,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
image.Width = 0;
image.Height = 0;
continue;
@@ -1935,7 +1948,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
image.BlurHash = string.Empty;
}
@@ -1945,7 +1958,7 @@ namespace Emby.Server.Implementations.Library
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
_logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
}
}
@@ -2503,7 +2516,7 @@ namespace Emby.Server.Implementations.Library
public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh)
{
var series = episode.Series;
bool? isAbsoluteNaming = series == null ? false : string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
bool? isAbsoluteNaming = series != null && string.Equals(series.DisplayOrder, "absolute", StringComparison.OrdinalIgnoreCase);
if (!isAbsoluteNaming.Value)
{
// In other words, no filter applied
@@ -2515,9 +2528,23 @@ namespace Emby.Server.Implementations.Library
var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
// TODO nullable - what are we trying to do there with empty episodeInfo?
var episodeInfo = episode.IsFileProtocol
? resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming) ?? new Naming.TV.EpisodeInfo(episode.Path)
: new Naming.TV.EpisodeInfo(episode.Path);
EpisodeInfo episodeInfo = null;
if (episode.IsFileProtocol)
{
episodeInfo = resolver.Resolve(episode.Path, isFolder, null, null, isAbsoluteNaming);
// Resolve from parent folder if it's not the Season folder
if (episodeInfo == null && episode.Parent.GetType() == typeof(Folder))
{
episodeInfo = resolver.Resolve(episode.Parent.Path, true, null, null, isAbsoluteNaming);
if (episodeInfo != null)
{
// add the container
episodeInfo.Container = Path.GetExtension(episode.Path)?.TrimStart('.');
}
}
}
episodeInfo ??= new EpisodeInfo(episode.Path);
try
{
@@ -2767,6 +2794,7 @@ namespace Emby.Server.Implementations.Library
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
{
string newPath;
if (ownerItem != null)
{
var libraryOptions = GetLibraryOptions(ownerItem);
@@ -2774,15 +2802,9 @@ namespace Emby.Server.Implementations.Library
{
foreach (var pathInfo in libraryOptions.PathInfos)
{
if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
{
continue;
}
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
return newPath;
}
}
}
@@ -2791,24 +2813,16 @@ namespace Emby.Server.Implementations.Library
var metadataPath = _configurationManager.Configuration.MetadataPath;
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
{
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
if (metadataSubstitutionResult.Item2)
{
return metadataSubstitutionResult.Item1;
}
return newPath;
}
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
if (!string.IsNullOrWhiteSpace(map.From))
if (path.TryReplaceSubPath(map.From, map.To, out newPath))
{
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
if (substitutionResult.Item2)
{
return substitutionResult.Item1;
}
return newPath;
}
}
@@ -2817,47 +2831,12 @@ namespace Emby.Server.Implementations.Library
public string SubstitutePath(string path, string from, string to)
{
return SubstitutePathInternal(path, from, to).Item1;
}
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
{
if (string.IsNullOrWhiteSpace(path))
if (path.TryReplaceSubPath(from, to, out var newPath))
{
throw new ArgumentNullException(nameof(path));
return newPath;
}
if (string.IsNullOrWhiteSpace(from))
{
throw new ArgumentNullException(nameof(from));
}
if (string.IsNullOrWhiteSpace(to))
{
throw new ArgumentNullException(nameof(to));
}
from = from.Trim();
to = to.Trim();
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
var changed = false;
if (!string.Equals(newPath, path, StringComparison.Ordinal))
{
if (to.IndexOf('/', StringComparison.Ordinal) != -1)
{
newPath = newPath.Replace('\\', '/');
}
else
{
newPath = newPath.Replace('/', '\\');
}
changed = true;
}
return new Tuple<string, bool>(newPath, changed);
return path;
}
private void SetExtraTypeFromFilename(Video item)
@@ -2914,6 +2893,12 @@ namespace Emby.Server.Implementations.Library
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
{
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc />
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
{
if (!item.SupportsPeople)
{
@@ -2921,6 +2906,8 @@ namespace Emby.Server.Implementations.Library
}
_itemRepository.UpdatePeople(item.Id, people);
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2956,7 +2943,7 @@ namespace Emby.Server.Implementations.Library
throw new InvalidOperationException();
}
public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
@@ -2990,9 +2977,9 @@ namespace Emby.Server.Implementations.Library
{
Directory.CreateDirectory(virtualFolderPath);
if (!string.IsNullOrEmpty(collectionType))
if (collectionType != null)
{
var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>());
}
@@ -3024,6 +3011,58 @@ namespace Emby.Server.Implementations.Library
}
}
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
{
var personsToSave = new List<BaseItem>();
foreach (var person in people)
{
cancellationToken.ThrowIfCancellationRequested();
var itemUpdateType = ItemUpdateType.MetadataDownload;
var saveEntity = false;
var personEntity = GetPerson(person.Name);
// if PresentationUniqueKey is empty it's likely a new item.
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
{
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true;
}
foreach (var id in person.ProviderIds)
{
if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
{
personEntity.SetProviderId(id.Key, id.Value);
saveEntity = true;
}
}
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
{
personEntity.SetImage(
new ItemImageInfo
{
Path = person.ImageUrl,
Type = ImageType.Primary
},
0);
saveEntity = true;
itemUpdateType = ItemUpdateType.ImageUpdate;
}
if (saveEntity)
{
personsToSave.Add(personEntity);
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
}
}
CreateItems(personsToSave, null, CancellationToken.None);
}
private void StartScanInBackground()
{
Task.Run(() =>

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -5,16 +7,17 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -23,14 +26,13 @@ namespace Emby.Server.Implementations.Library
{
private readonly IMediaEncoder _mediaEncoder;
private readonly ILogger _logger;
private readonly IJsonSerializer _json;
private readonly IApplicationPaths _appPaths;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IJsonSerializer json, IApplicationPaths appPaths)
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
{
_mediaEncoder = mediaEncoder;
_logger = logger;
_json = json;
_appPaths = appPaths;
}
@@ -47,7 +49,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
mediaInfo = _json.DeserializeFromFile<MediaInfo>(cacheFilePath);
await using FileStream jsonStream = File.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -83,7 +86,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
_json.SerializeToFile(mediaInfo, cacheFilePath);
await using FileStream createStream = File.OpenWrite(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -6,12 +8,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@@ -23,7 +27,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Library
@@ -36,7 +39,6 @@ namespace Emby.Server.Implementations.Library
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
private readonly ILibraryManager _libraryManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
private readonly ILogger<MediaSourceManager> _logger;
private readonly IUserDataManager _userDataManager;
@@ -46,6 +48,7 @@ namespace Emby.Server.Implementations.Library
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private IMediaSourceProvider[] _providers;
@@ -56,7 +59,6 @@ namespace Emby.Server.Implementations.Library
IUserManager userManager,
ILibraryManager libraryManager,
ILogger<MediaSourceManager> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IUserDataManager userDataManager,
IMediaEncoder mediaEncoder)
@@ -65,7 +67,6 @@ namespace Emby.Server.Implementations.Library
_userManager = userManager;
_libraryManager = libraryManager;
_logger = logger;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
_mediaEncoder = mediaEncoder;
@@ -200,10 +201,15 @@ namespace Emby.Server.Implementations.Library
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding);
source.SupportsDirectStream = user.HasPermission(PermissionKind.EnablePlaybackRemuxing);
}
}
}
return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
return SortMediaSources(list);
}
public MediaProtocol GetPathProtocol(string path)
@@ -437,7 +443,7 @@ namespace Emby.Server.Implementations.Library
}
}
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
{
return sources.OrderBy(i =>
{
@@ -452,8 +458,9 @@ namespace Emby.Server.Implementations.Library
{
var stream = i.VideoStream;
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
return stream?.Width ?? 0;
})
.Where(i => i.Type != MediaSourceType.Placeholder)
.ToList();
}
@@ -504,7 +511,7 @@ namespace Emby.Server.Implementations.Library
// hack - these two values were taken from LiveTVMediaSourceProvider
string cacheKey = request.OpenToken;
await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths)
await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
.ConfigureAwait(false);
}
@@ -516,9 +523,9 @@ namespace Emby.Server.Implementations.Library
}
// TODO: @bond Fix
var json = _jsonSerializer.SerializeToString(mediaSource);
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
_logger.LogInformation("Live stream opened: " + json);
var clone = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json);
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
if (!request.UserId.Equals(Guid.Empty))
{
@@ -585,18 +592,9 @@ namespace Emby.Server.Implementations.Library
public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken)
{
var info = _openStreams.Values.FirstOrDefault(i =>
{
var liveStream = i as ILiveStream;
if (liveStream != null)
{
return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase);
}
var info = _openStreams.FirstOrDefault(i => i.Value != null && string.Equals(i.Value.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase));
return false;
});
return Task.FromResult(info as IDirectStreamProvider);
return Task.FromResult(info.Value as IDirectStreamProvider);
}
public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken)
@@ -643,7 +641,8 @@ namespace Emby.Server.Implementations.Library
{
try
{
mediaInfo = _jsonSerializer.DeserializeFromFile<MediaInfo>(cacheFilePath);
await using FileStream jsonStream = File.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
}
@@ -679,7 +678,8 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath != null)
{
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
_jsonSerializer.SerializeToFile(mediaInfo, cacheFilePath);
await using FileStream createStream = File.Create(cacheFilePath);
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -100,8 +102,7 @@ namespace Emby.Server.Implementations.Library
public List<BaseItem> GetInstantMixFromItem(BaseItem item, User user, DtoOptions dtoOptions)
{
var genre = item as MusicGenre;
if (genre != null)
if (item is MusicGenre genre)
{
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}

View File

@@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Text.RegularExpressions;
using System.Diagnostics.CodeAnalysis;
using MediaBrowser.Common.Providers;
namespace Emby.Server.Implementations.Library
{
@@ -41,11 +40,78 @@ namespace Emby.Server.Implementations.Library
// for imdbid we also accept pattern matching
if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{
var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
return m.Success ? m.Value : null;
var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId);
return match ? imdbId.ToString() : null;
}
return null;
}
/// <summary>
/// Replaces a sub path with another sub path and normalizes the final path.
/// </summary>
/// <param name="path">The original path.</param>
/// <param name="subPath">The original sub path.</param>
/// <param name="newSubPath">The new sub path.</param>
/// <param name="newPath">The result of the sub path replacement</param>
/// <returns>The path after replacing the sub path.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
public static bool TryReplaceSubPath(
[NotNullWhen(true)] this string? path,
[NotNullWhen(true)] string? subPath,
[NotNullWhen(true)] string? newSubPath,
[NotNullWhen(true)] out string? newPath)
{
newPath = null;
if (string.IsNullOrEmpty(path)
|| string.IsNullOrEmpty(subPath)
|| string.IsNullOrEmpty(newSubPath)
|| subPath.Length > path.Length)
{
return false;
}
char oldDirectorySeparatorChar;
char newDirectorySeparatorChar;
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
// The reasoning behind this is that a forward slash likely means it's a Linux path and
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
if (newSubPath.Contains('/', StringComparison.Ordinal))
{
oldDirectorySeparatorChar = '\\';
newDirectorySeparatorChar = '/';
}
else
{
oldDirectorySeparatorChar = '/';
newDirectorySeparatorChar = '\\';
}
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
// when the sub path matches a similar but in-complete subpath
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (path.Length > subPath.Length
&& !oldSubPathEndsWithSeparator
&& path[subPath.Length] != newDirectorySeparatorChar)
{
return false;
}
var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
// Ensure that the path with the old subpath removed starts with a leading dir separator
int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
return true;
}
}
}

View File

@@ -18,11 +18,10 @@ namespace Emby.Server.Implementations.Library
/// </summary>
/// <param name="item">The item.</param>
/// <param name="parent">The parent.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="directoryService">The directory service.</param>
/// <exception cref="ArgumentException">Item must have a path</exception>
public static void SetInitialItemValues(BaseItem item, Folder parent, IFileSystem fileSystem, ILibraryManager libraryManager, IDirectoryService directoryService)
/// <exception cref="ArgumentException">Item must have a path.</exception>
public static void SetInitialItemValues(BaseItem item, Folder? parent, ILibraryManager libraryManager, IDirectoryService directoryService)
{
// This version of the below method has no ItemResolveArgs, so we have to require the path already being set
if (string.IsNullOrEmpty(item.Path))
@@ -43,9 +42,14 @@ namespace Emby.Server.Implementations.Library
// Make sure DateCreated and DateModified have values
var fileInfo = directoryService.GetFile(item.Path);
SetDateCreated(item, fileSystem, fileInfo);
if (fileInfo == null)
{
throw new FileNotFoundException("Can't find item path.", item.Path);
}
EnsureName(item, item.Path, fileInfo);
SetDateCreated(item, fileInfo);
EnsureName(item, fileInfo);
}
/// <summary>
@@ -72,9 +76,9 @@ namespace Emby.Server.Implementations.Library
item.Id = libraryManager.GetNewItemId(item.Path, item.GetType());
// Make sure the item has a name
EnsureName(item, item.Path, args.FileInfo);
EnsureName(item, args.FileInfo);
item.IsLocked = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1 ||
item.IsLocked = item.Path.Contains("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) ||
item.GetParents().Any(i => i.IsLocked);
// Make sure DateCreated and DateModified have values
@@ -84,28 +88,15 @@ namespace Emby.Server.Implementations.Library
/// <summary>
/// Ensures the name.
/// </summary>
private static void EnsureName(BaseItem item, string fullPath, FileSystemMetadata fileInfo)
private static void EnsureName(BaseItem item, FileSystemMetadata fileInfo)
{
// If the subclass didn't supply a name, add it here
if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(fullPath))
if (string.IsNullOrEmpty(item.Name) && !string.IsNullOrEmpty(item.Path))
{
var fileName = fileInfo == null ? Path.GetFileName(fullPath) : fileInfo.Name;
item.Name = GetDisplayName(fileName, fileInfo != null && fileInfo.IsDirectory);
item.Name = fileInfo.IsDirectory ? fileInfo.Name : Path.GetFileNameWithoutExtension(fileInfo.Name);
}
}
/// <summary>
/// Gets the display name.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isDirectory">if set to <c>true</c> [is directory].</param>
/// <returns>System.String.</returns>
private static string GetDisplayName(string path, bool isDirectory)
{
return isDirectory ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path);
}
/// <summary>
/// Ensures DateCreated and DateModified have values.
/// </summary>
@@ -114,21 +105,6 @@ namespace Emby.Server.Implementations.Library
/// <param name="args">The args.</param>
private static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args)
{
if (fileSystem == null)
{
throw new ArgumentNullException(nameof(fileSystem));
}
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
if (args == null)
{
throw new ArgumentNullException(nameof(args));
}
// See if a different path came out of the resolver than what went in
if (!fileSystem.AreEqual(args.Path, item.Path))
{
@@ -136,7 +112,7 @@ namespace Emby.Server.Implementations.Library
if (childData != null)
{
SetDateCreated(item, fileSystem, childData);
SetDateCreated(item, childData);
}
else
{
@@ -144,17 +120,17 @@ namespace Emby.Server.Implementations.Library
if (fileData.Exists)
{
SetDateCreated(item, fileSystem, fileData);
SetDateCreated(item, fileData);
}
}
}
else
{
SetDateCreated(item, fileSystem, args.FileInfo);
SetDateCreated(item, args.FileInfo);
}
}
private static void SetDateCreated(BaseItem item, IFileSystem fileSystem, FileSystemMetadata info)
private static void SetDateCreated(BaseItem item, FileSystemMetadata? info)
{
var config = BaseItem.ConfigurationManager.GetMetadataConfiguration();
@@ -163,7 +139,7 @@ namespace Emby.Server.Implementations.Library
// directoryService.getFile may return null
if (info != null)
{
var dateCreated = fileSystem.GetCreationTimeUtc(info);
var dateCreated = info.CreationTimeUtc;
if (dateCreated.Equals(DateTime.MinValue))
{

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Fourth;
public override ResolverPriority Priority => ResolverPriority.Fifth;
public MultiItemResolverResult ResolveMultiple(
Folder parent,
@@ -201,6 +203,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
continue;
}
if (resolvedItem.Files.Count == 0)
{
continue;
}
var firstMedia = resolvedItem.Files[0];
var libraryItem = new T

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@@ -40,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Second;
public override ResolverPriority Priority => ResolverPriority.Third;
/// <summary>
/// Resolves the specified args.

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Linq;
using System.Threading.Tasks;
@@ -79,11 +81,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
return new MusicArtist();
}
if (_config.Configuration.EnableSimpleArtistDetection)
{
return null;
}
// Avoid mis-identifying top folders
if (args.Parent.IsRoot)
{

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -30,7 +32,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
protected override T Resolve(ItemResolveArgs args)
public override T Resolve(ItemResolveArgs args)
{
return ResolveVideo<T>(args, false);
}
@@ -42,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// <param name="args">The args.</param>
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
/// <returns>``0.</returns>
protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
where TVideoType : Video, new()
{
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
@@ -165,13 +167,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
protected void SetVideoType(Video video, VideoFileInfo videoInfo)
{
var extension = Path.GetExtension(video.Path);
video.VideoType = string.Equals(extension, ".iso", StringComparison.OrdinalIgnoreCase) ||
string.Equals(extension, ".img", StringComparison.OrdinalIgnoreCase) ?
VideoType.Iso :
VideoType.VideoFile;
var extension = Path.GetExtension(video.Path.AsSpan());
video.VideoType = extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)
|| extension.Equals(".img", StringComparison.OrdinalIgnoreCase)
? VideoType.Iso
: VideoType.VideoFile;
video.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
video.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
video.IsPlaceHolder = videoInfo.IsStub;
if (videoInfo.IsStub)
@@ -193,11 +195,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (video.VideoType == VideoType.Iso)
{
if (video.Path.IndexOf("dvd", StringComparison.OrdinalIgnoreCase) != -1)
if (video.Path.Contains("dvd", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.Dvd;
}
else if (video.Path.IndexOf("bluray", StringComparison.OrdinalIgnoreCase) != -1)
else if (video.Path.Contains("bluray", StringComparison.OrdinalIgnoreCase))
{
video.IsoType = IsoType.BluRay;
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -11,9 +13,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
public override Book Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();

View File

@@ -1,3 +1,5 @@
#nullable disable
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;

View File

@@ -1,3 +1,5 @@
#nullable disable
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -11,6 +13,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
public abstract class ItemResolver<T> : IItemResolver
where T : BaseItem, new()
{
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public virtual ResolverPriority Priority => ResolverPriority.First;
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -21,12 +29,6 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
/// <summary>
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public virtual ResolverPriority Priority => ResolverPriority.First;
/// <summary>
/// Sets initial values on the newly resolved item.
/// </summary>

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.IO;
using MediaBrowser.Controller.Entities;

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
/// Gets the priority.
/// </summary>
/// <value>The priority.</value>
public override ResolverPriority Priority => ResolverPriority.Third;
public override ResolverPriority Priority => ResolverPriority.Fourth;
/// <inheritdoc />
public MultiItemResolverResult ResolveMultiple(
@@ -69,6 +71,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return result;
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
public override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
private MultiItemResolverResult ResolveMultipleInternal(
Folder parent,
List<FileSystemMetadata> files,
@@ -216,110 +322,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Video.</returns>
protected override Video Resolve(ItemResolveArgs args)
{
var collectionType = args.GetCollectionType();
// Find movies with their own folders
if (args.IsDirectory)
{
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
var files = args.FileSystemChildren
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
.ToList();
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
}
if (string.IsNullOrEmpty(collectionType))
{
// Owned items will be caught by the plain video resolver
if (args.Parent == null)
{
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
return null;
}
if (args.HasParent<Series>())
{
return null;
}
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
}
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
}
return null;
}
// Handle owned items
if (args.Parent == null)
{
return base.Resolve(args);
}
if (IsInvalid(args.Parent, collectionType))
{
return null;
}
Video item = null;
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<MusicVideo>(args, false);
}
// To find a movie file, the collection type must be movies or boxsets
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Movie>(args, true);
}
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
{
item = ResolveVideo<Video>(args, false);
}
else if (string.IsNullOrEmpty(collectionType))
{
if (args.HasParent<Series>())
{
return null;
}
item = ResolveVideo<Video>(args, false);
}
if (item != null)
{
item.IsInMixedFolder = true;
}
return item;
}
/// <summary>
/// Sets the initial item values.
/// </summary>
@@ -376,7 +378,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
var multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.GetLibraryOptions();
var libraryOptions = args.LibraryOptions;
var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>();

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -13,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class PhotoAlbumResolver : FolderResolver<PhotoAlbum>
{
private readonly IImageProcessor _imageProcessor;
private ILibraryManager _libraryManager;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
@@ -26,6 +28,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
_libraryManager = libraryManager;
}
/// <inheritdoc />
public override ResolverPriority Priority => ResolverPriority.Second;
/// <summary>
/// Resolves the specified args.
/// </summary>
@@ -39,8 +44,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase) ||
(string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
{
if (HasPhotos(args))
{
@@ -84,8 +89,5 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false;
}
/// <inheritdoc />
public override ResolverPriority Priority => ResolverPriority.Second;
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
var collectionType = args.CollectionType;
if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.GetLibraryOptions().EnablePhotos))
|| (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
{
if (IsImageFile(args.Path, _imageProcessor))
{

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -41,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
}
// It's a directory-based playlist if the directory contains a playlist file
var filePaths = Directory.EnumerateFiles(args.Path);
var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
{
return new Playlist
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
Path = args.Path,
Name = Path.GetFileNameWithoutExtension(args.Path),
IsInMixedFolder = true
IsInMixedFolder = true,
PlaylistMediaType = MediaType.Audio
};
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,5 +1,8 @@
#nullable disable
using System;
using System.Linq;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@@ -11,12 +14,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class EpisodeResolver : BaseVideoResolver<Episode>
{
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
/// <summary>
/// Resolves the specified args.
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Episode.</returns>
protected override Episode Resolve(ItemResolveArgs args)
public override Episode Resolve(ItemResolveArgs args)
{
var parent = args.Parent;
@@ -25,30 +37,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
var season = parent as Season;
// Just in case the user decided to nest episodes.
// Not officially supported but in some cases we can handle it.
if (season == null)
{
season = parent.GetParents().OfType<Season>().FirstOrDefault();
}
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
var season = parent as Season ?? parent.GetParents().OfType<Season>().FirstOrDefault();
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
// Also handle flat tv folders
if (season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
if ((season != null ||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
args.HasParent<Series>())
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
{
var episode = ResolveVideo<Episode>(args, false);
if (episode != null)
{
var series = parent as Series;
if (series == null)
{
series = parent.GetParents().OfType<Series>().FirstOrDefault();
}
var series = parent as Series ?? parent.GetParents().OfType<Series>().FirstOrDefault();
if (series != null)
{
@@ -74,14 +79,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
/// <summary>
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
public EpisodeResolver(ILibraryManager libraryManager)
: base(libraryManager)
{
}
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
using System.Globalization;
using Emby.Naming.TV;
using MediaBrowser.Controller.Entities.TV;
@@ -88,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameSeasonNumber"),
seasonNumber,
args.GetLibraryOptions().PreferredMetadataLanguage);
args.LibraryOptions.PreferredMetadataLanguage);
}
return season;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -19,19 +21,16 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
/// </summary>
public class SeriesResolver : FolderResolver<Series>
{
private readonly IFileSystem _fileSystem;
private readonly ILogger<SeriesResolver> _logger;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="SeriesResolver"/> class.
/// </summary>
/// <param name="fileSystem">The file system.</param>
/// <param name="logger">The logger.</param>
/// <param name="libraryManager">The library manager.</param>
public SeriesResolver(IFileSystem fileSystem, ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
{
_fileSystem = fileSystem;
_logger = logger;
_libraryManager = libraryManager;
}
@@ -59,15 +58,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var collectionType = args.GetCollectionType();
if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
// if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
//{
// return new Series
// {
// Path = args.Path,
// Name = Path.GetFileName(args.Path)
// };
//}
var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
{
@@ -100,7 +90,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return null;
}
if (IsSeriesFolder(args.Path, args.FileSystemChildren, args.DirectoryService, _fileSystem, _logger, _libraryManager, false))
if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
{
return new Series
{
@@ -117,8 +107,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
public static bool IsSeriesFolder(
string path,
IEnumerable<FileSystemMetadata> fileSystemChildren,
IDirectoryService directoryService,
IFileSystem fileSystem,
ILogger<SeriesResolver> logger,
ILibraryManager libraryManager,
bool isTvContentType)
@@ -127,7 +115,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
{
if (child.IsDirectory)
{
if (IsSeasonFolder(child.FullName, isTvContentType, libraryManager))
if (IsSeasonFolder(child.FullName, isTvContentType))
{
logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
return true;
@@ -160,32 +148,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
return false;
}
/// <summary>
/// Determines whether [is place holder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [is place holder] [the specified path]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException">path</exception>
private static bool IsVideoPlaceHolder(string path)
{
if (string.IsNullOrEmpty(path))
{
throw new ArgumentNullException(nameof(path));
}
var extension = Path.GetExtension(path);
return string.Equals(extension, ".disc", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether [is season folder] [the specified path].
/// </summary>
/// <param name="path">The path.</param>
/// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
/// <param name="libraryManager">The library manager.</param>
/// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
private static bool IsSeasonFolder(string path, bool isTvContentType, ILibraryManager libraryManager)
private static bool IsSeasonFolder(string path, bool isTvContentType)
{
var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using MediaBrowser.Controller.Entities;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -12,7 +14,6 @@ using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
@@ -47,7 +48,7 @@ namespace Emby.Server.Implementations.Library
if (query.Limit.HasValue)
{
results = results.GetRange(0, query.Limit.Value);
results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count));
}
return new QueryResult<SearchHintInfo>

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -13,6 +15,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
@@ -219,7 +222,7 @@ namespace Emby.Server.Implementations.Library
var hasRuntime = runtimeTicks > 0;
// If a position has been reported, and if we know the duration
if (positionTicks > 0 && hasRuntime)
if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
{
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
@@ -238,13 +241,30 @@ namespace Emby.Server.Implementations.Library
{
// Enforce MinResumeDuration
var durationSeconds = TimeSpan.FromTicks(runtimeTicks).TotalSeconds;
if (durationSeconds < _config.Configuration.MinResumeDurationSeconds && !(item is Book))
if (durationSeconds < _config.Configuration.MinResumeDurationSeconds)
{
positionTicks = 0;
data.Played = playedToCompletion = true;
}
}
}
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
{
var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
{
// ignore progress during the beginning
positionTicks = 0;
}
else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
{
// mark as completed close to the end
positionTicks = 0;
data.Played = playedToCompletion = true;
}
}
else if (!hasRuntime)
{
// If we don't know the runtime we'll just have to assume it was fully played

View File

@@ -1,8 +1,9 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
@@ -129,23 +130,23 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
list = list.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes).Contains(i.Id)).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
return list
.OrderBy(i =>
{
var index = orders.IndexOf(i.Id.ToString("D", CultureInfo.InvariantCulture));
var index = Array.IndexOf(orders, i.Id);
if (index == -1
&& i is UserView view
&& view.DisplayParentId != Guid.Empty)
{
index = orders.IndexOf(view.DisplayParentId.ToString("D", CultureInfo.InvariantCulture));
index = Array.IndexOf(orders, view.DisplayParentId);
}
return index == -1 ? int.MaxValue : index;
@@ -280,8 +281,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
.Contains(i.Id))
.ToList();
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -45,7 +47,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
onStarted();
@@ -70,7 +73,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
onStarted();

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -17,7 +19,6 @@ using Jellyfin.Data.Enums;
using Jellyfin.Data.Events;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
@@ -36,7 +37,6 @@ using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -51,7 +51,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger<EmbyTV> _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerConfigurationManager _config;
private readonly IJsonSerializer _jsonSerializer;
private readonly ItemDataProvider<SeriesTimerInfo> _seriesTimerProvider;
private readonly TimerManager _timerProvider;
@@ -81,7 +80,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IStreamHelper streamHelper,
IMediaSourceManager mediaSourceManager,
ILogger<EmbyTV> logger,
IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IServerConfigurationManager config,
ILiveTvManager liveTvManager,
@@ -103,12 +101,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_providerManager = providerManager;
_mediaEncoder = mediaEncoder;
_liveTvManager = (LiveTvManager)liveTvManager;
_jsonSerializer = jsonSerializer;
_mediaSourceManager = mediaSourceManager;
_streamHelper = streamHelper;
_seriesTimerProvider = new SeriesTimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(jsonSerializer, _logger, Path.Combine(DataPath, "timers.json"));
_seriesTimerProvider = new SeriesTimerManager(_logger, Path.Combine(DataPath, "seriestimers.json"));
_timerProvider = new TimerManager(_logger, Path.Combine(DataPath, "timers.json"));
_timerProvider.TimerFired += OnTimerProviderTimerFired;
_config.NamedConfigurationUpdated += OnNamedConfigurationUpdated;
@@ -806,22 +803,22 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public ActiveRecordingInfo GetActiveRecordingInfo(string path)
{
if (string.IsNullOrWhiteSpace(path))
if (string.IsNullOrWhiteSpace(path) || _activeRecordings.IsEmpty)
{
return null;
}
foreach (var recording in _activeRecordings.Values)
foreach (var (_, recordingInfo) in _activeRecordings)
{
if (string.Equals(recording.Path, path, StringComparison.Ordinal) && !recording.CancellationTokenSource.IsCancellationRequested)
if (string.Equals(recordingInfo.Path, path, StringComparison.Ordinal) && !recordingInfo.CancellationTokenSource.IsCancellationRequested)
{
var timer = recording.Timer;
var timer = recordingInfo.Timer;
if (timer.Status != RecordingStatus.InProgress)
{
return null;
}
return recording;
return recordingInfo;
}
}
@@ -1052,7 +1049,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
IgnoreIndex = true
};
await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _config.CommonApplicationPaths)
await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths)
.AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false);
return new List<MediaSourceInfo>
@@ -1626,16 +1623,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
}
return _activeRecordings
.Values
.ToList()
.Any(i => string.Equals(i.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
.Any(i => string.Equals(i.Value.Path, path, StringComparison.OrdinalIgnoreCase) && !string.Equals(i.Value.Timer.Id, timerId, StringComparison.OrdinalIgnoreCase));
}
private IRecorder GetRecorder(MediaSourceInfo mediaSource)
{
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, _config);
return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _config);
}
return new DirectRecorder(_logger, _httpClientFactory, _streamHelper);
@@ -1860,7 +1855,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -1924,7 +1920,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return;
}
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
var settings = new XmlWriterSettings
{
@@ -2242,14 +2239,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var enabledTimersForSeries = new List<TimerInfo>();
foreach (var timer in allTimers)
{
var existingTimer = _timerProvider.GetTimer(timer.Id);
if (existingTimer == null)
{
existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId)
var existingTimer = _timerProvider.GetTimer(timer.Id)
?? (string.IsNullOrWhiteSpace(timer.ProgramId)
? null
: _timerProvider.GetTimerByProgramId(timer.ProgramId);
}
: _timerProvider.GetTimerByProgramId(timer.ProgramId));
if (existingTimer == null)
{
@@ -2608,7 +2601,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Movies",
CollectionType = CollectionType.Movies
CollectionType = CollectionTypeOptions.Movies
};
}
@@ -2619,7 +2612,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
Locations = new string[] { customPath },
Name = "Recorded Shows",
CollectionType = CollectionType.TvShows
CollectionType = CollectionTypeOptions.TvShows
};
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -6,16 +8,18 @@ using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -25,10 +29,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IServerApplicationPaths _appPaths;
private readonly IJsonSerializer _json;
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited;
private Stream _logFileStream;
private string _targetPath;
@@ -38,13 +41,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
ILogger logger,
IMediaEncoder mediaEncoder,
IServerApplicationPaths appPaths,
IJsonSerializer json,
IServerConfigurationManager serverConfigurationManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_appPaths = appPaths;
_json = json;
_serverConfigurationManager = serverConfigurationManager;
}
@@ -66,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_logger.LogInformation("Recording completed to file {0}", targetFile);
}
private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
private async Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
{
_targetPath = targetFile;
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
@@ -95,8 +96,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
// FFMpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory.
_logFileStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(_json.SerializeToString(mediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine);
_logFileStream.Write(commandLineLogMessageBytes, 0, commandLineLogMessageBytes.Length);
await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
_process = new Process
{
@@ -115,8 +116,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
_ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream);
_logger.LogInformation("ffmpeg recording process started for {0}", _targetPath);
return _taskCompletionSource.Task;
}
private string GetCommandLineArgs(MediaSourceInfo mediaSource, string inputTempFile, string targetFile, TimeSpan duration)
@@ -311,13 +310,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
using (var reader = new StreamReader(source))
{
while (!reader.EndOfStream)
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
var line = await reader.ReadLineAsync().ConfigureAwait(false);
var bytes = Encoding.UTF8.GetBytes(Environment.NewLine + line);
await target.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false);
await target.WriteAsync(bytes.AsMemory()).ConfigureAwait(false);
await target.FlushAsync().ConfigureAwait(false);
}
}

View File

@@ -6,58 +6,46 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
internal class EpgChannelData
{
private readonly Dictionary<string, ChannelInfo> _channelsById;
private readonly Dictionary<string, ChannelInfo> _channelsByNumber;
private readonly Dictionary<string, ChannelInfo> _channelsByName;
public EpgChannelData(IEnumerable<ChannelInfo> channels)
{
ChannelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
ChannelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
ChannelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
_channelsById = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
_channelsByNumber = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
_channelsByName = new Dictionary<string, ChannelInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var channel in channels)
{
ChannelsById[channel.Id] = channel;
_channelsById[channel.Id] = channel;
if (!string.IsNullOrEmpty(channel.Number))
{
ChannelsByNumber[channel.Number] = channel;
_channelsByNumber[channel.Number] = channel;
}
var normalizedName = NormalizeName(channel.Name ?? string.Empty);
if (!string.IsNullOrWhiteSpace(normalizedName))
{
ChannelsByName[normalizedName] = channel;
_channelsByName[normalizedName] = channel;
}
}
}
private Dictionary<string, ChannelInfo> ChannelsById { get; set; }
public ChannelInfo? GetChannelById(string id)
=> _channelsById.GetValueOrDefault(id);
private Dictionary<string, ChannelInfo> ChannelsByNumber { get; set; }
public ChannelInfo? GetChannelByNumber(string number)
=> _channelsByNumber.GetValueOrDefault(number);
private Dictionary<string, ChannelInfo> ChannelsByName { get; set; }
public ChannelInfo GetChannelById(string id)
{
ChannelsById.TryGetValue(id, out var result);
return result;
}
public ChannelInfo GetChannelByNumber(string number)
{
ChannelsByNumber.TryGetValue(number, out var result);
return result;
}
public ChannelInfo GetChannelByName(string name)
{
ChannelsByName.TryGetValue(name, out var result);
return result;
}
public ChannelInfo? GetChannelByName(string name)
=> _channelsByName.GetValueOrDefault(name);
public static string NormalizeName(string value)
{

View File

@@ -1,10 +1,13 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using MediaBrowser.Model.Serialization;
using System.Text.Json;
using MediaBrowser.Common.Json;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -12,18 +15,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
public class ItemDataProvider<T>
where T : class
{
private readonly IJsonSerializer _jsonSerializer;
private readonly string _dataPath;
private readonly object _fileDataLock = new object();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private T[] _items;
public ItemDataProvider(
IJsonSerializer jsonSerializer,
ILogger logger,
string dataPath,
Func<T, T, bool> equalityComparer)
{
_jsonSerializer = jsonSerializer;
Logger = logger;
_dataPath = dataPath;
EqualityComparer = equalityComparer;
@@ -46,10 +47,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
try
{
_items = _jsonSerializer.DeserializeFromFile<T[]>(_dataPath);
var bytes = File.ReadAllBytes(_dataPath);
_items = JsonSerializer.Deserialize<T[]>(bytes, _jsonOptions);
return;
}
catch (Exception ex)
catch (JsonException ex)
{
Logger.LogError(ex, "Error deserializing {Path}", _dataPath);
}
@@ -61,7 +63,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private void SaveList()
{
Directory.CreateDirectory(Path.GetDirectoryName(_dataPath));
_jsonSerializer.SerializeToFile(_items, _dataPath);
var jsonString = JsonSerializer.Serialize(_items, _jsonOptions);
File.WriteAllText(_dataPath, jsonString);
}
public IReadOnlyList<T> GetAll()

View File

@@ -6,7 +6,7 @@ using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
internal class RecordingHelper
internal static class RecordingHelper
{
public static DateTime GetStartTime(TimerInfo timer)
{
@@ -70,17 +70,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private static string GetDateString(DateTime date)
{
date = date.ToLocalTime();
return string.Format(
CultureInfo.InvariantCulture,
"{0}_{1}_{2}_{3}_{4}_{5}",
date.Year.ToString("0000", CultureInfo.InvariantCulture),
date.Month.ToString("00", CultureInfo.InvariantCulture),
date.Day.ToString("00", CultureInfo.InvariantCulture),
date.Hour.ToString("00", CultureInfo.InvariantCulture),
date.Minute.ToString("00", CultureInfo.InvariantCulture),
date.Second.ToString("00", CultureInfo.InvariantCulture));
return date.ToLocalTime().ToString("yyyy_MM_dd_HH_mm_ss", CultureInfo.InvariantCulture);
}
}
}

View File

@@ -2,15 +2,14 @@
using System;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo>
{
public SeriesTimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
: base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
public SeriesTimerManager(ILogger logger, string dataPath)
: base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -8,7 +10,6 @@ using System.Threading;
using Jellyfin.Data.Events;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV
@@ -17,8 +18,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
private readonly ConcurrentDictionary<string, Timer> _timers = new ConcurrentDictionary<string, Timer>(StringComparer.OrdinalIgnoreCase);
public TimerManager(IJsonSerializer jsonSerializer, ILogger logger, string dataPath)
: base(jsonSerializer, logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
public TimerManager(ILogger logger, string dataPath)
: base(logger, dataPath, (r1, r2) => string.Equals(r1.Id, r2.Id, StringComparison.OrdinalIgnoreCase))
{
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -9,16 +11,17 @@ using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.Listings
@@ -28,24 +31,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private const string ApiUrl = "https://json.schedulesdirect.org/20141201";
private readonly ILogger<SchedulesDirect> _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly IHttpClientFactory _httpClientFactory;
private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
private readonly IApplicationHost _appHost;
private readonly ICryptoProvider _cryptoProvider;
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private DateTime _lastErrorResponse;
public SchedulesDirect(
ILogger<SchedulesDirect> logger,
IJsonSerializer jsonSerializer,
IHttpClientFactory httpClientFactory,
IApplicationHost appHost,
ICryptoProvider cryptoProvider)
{
_logger = logger;
_jsonSerializer = jsonSerializer;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
_cryptoProvider = cryptoProvider;
@@ -104,7 +105,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
}
};
var requestString = _jsonSerializer.SerializeToString(requestList);
var requestString = JsonSerializer.Serialize(requestList, _jsonOptions);
_logger.LogDebug("Request string for schedules is: {RequestString}", requestString);
using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules");
@@ -112,7 +113,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false);
var dailySchedules = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId);
using var programRequestOptions = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/programs");
@@ -123,12 +124,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false);
var programDetails = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
var programIdsWithImages =
programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID)
.ToList();
var programIdsWithImages = programDetails
.Where(p => p.hasImageArtwork).Select(p => p.programID)
.ToList();
var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false);
@@ -183,8 +184,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
private static int GetSizeOrder(ScheduleDirect.ImageData image)
{
if (!string.IsNullOrWhiteSpace(image.height)
&& int.TryParse(image.height, out int value))
if (int.TryParse(image.height, out int value))
{
return value;
}
@@ -479,7 +479,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>(response).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -508,7 +508,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root != null)
{
@@ -649,7 +649,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
{
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
@@ -705,7 +705,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
}
@@ -777,27 +777,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
_logger.LogInformation("Mapping Stations to Channel");
var allStations = root.stations ?? new List<ScheduleDirect.Station>();
var map = root.map;
int len = map.Count;
var array = new List<ChannelInfo>(len);
for (int i = 0; i < len; i++)
var list = new List<ChannelInfo>(map.Count);
foreach (var channel in map)
{
var channelNumber = GetChannelNumber(map[i]);
var channelNumber = GetChannelNumber(channel);
var station = allStations.Find(item => string.Equals(item.stationID, map[i].stationID, StringComparison.OrdinalIgnoreCase));
if (station == null)
{
station = new ScheduleDirect.Station
var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase))
?? new ScheduleDirect.Station
{
stationID = map[i].stationID
stationID = channel.stationID
};
}
var channelInfo = new ChannelInfo
{
@@ -812,10 +808,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
channelInfo.ImageUrl = station.logo.URL;
}
array[i] = channelInfo;
list.Add(channelInfo);
}
return array;
return list;
}
private static string NormalizeName(string value)

View File

@@ -1,10 +1,11 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading;

View File

@@ -1,21 +1,23 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.LiveTv;
namespace Emby.Server.Implementations.LiveTv
{
/// <summary>
/// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />.
/// </summary>
public class LiveTvConfigurationFactory : IConfigurationFactory
{
/// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new ConfigurationStore[]
{
new ConfigurationStore
{
ConfigurationType = typeof(LiveTvOptions),
Key = "livetv"
ConfigurationType = typeof(LiveTvOptions),
Key = "livetv"
}
};
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -987,10 +989,7 @@ namespace Emby.Server.Implementations.LiveTv
var externalProgramId = programTuple.Item2;
string externalSeriesId = programTuple.Item3;
if (timerList == null)
{
timerList = (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
}
timerList ??= (await GetTimersInternal(new TimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var timer = timerList.FirstOrDefault(i => string.Equals(i.ProgramId, externalProgramId, StringComparison.OrdinalIgnoreCase));
var foundSeriesTimer = false;
@@ -1018,10 +1017,7 @@ namespace Emby.Server.Implementations.LiveTv
continue;
}
if (seriesTimerList == null)
{
seriesTimerList = (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
}
seriesTimerList ??= (await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false)).Items;
var seriesTimer = seriesTimerList.FirstOrDefault(i => string.Equals(i.SeriesId, externalSeriesId, StringComparison.OrdinalIgnoreCase));
@@ -1974,10 +1970,7 @@ namespace Emby.Server.Implementations.LiveTv
};
}
if (service == null)
{
service = _services[0];
}
service ??= _services[0];
var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false);
@@ -2239,7 +2232,7 @@ namespace Emby.Server.Implementations.LiveTv
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)
{
info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.Serialize(info));
info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2273,7 +2266,7 @@ namespace Emby.Server.Implementations.LiveTv
if (dataSourceChanged)
{
_taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
return info;
@@ -2283,7 +2276,7 @@ namespace Emby.Server.Implementations.LiveTv
{
// Hack to make the object a pure ListingsProviderInfo instead of an AddListingProvider
// ServerConfiguration.SaveConfiguration crashes during xml serialization for AddListingProvider
info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.Serialize(info));
info = JsonSerializer.Deserialize<ListingsProviderInfo>(JsonSerializer.SerializeToUtf8Bytes(info));
var provider = _listingProviders.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase));
@@ -2316,7 +2309,7 @@ namespace Emby.Server.Implementations.LiveTv
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return info;
}
@@ -2328,7 +2321,7 @@ namespace Emby.Server.Implementations.LiveTv
config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray();
_config.SaveConfiguration("livetv", config);
_taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
}
public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelId, string providerChannelId)
@@ -2362,7 +2355,7 @@ namespace Emby.Server.Implementations.LiveTv
var tunerChannelMappings =
tunerChannels.Select(i => GetTunerChannelMapping(i, mappings, providerChannels)).ToList();
_taskManager.CancelIfRunningAndQueue<RefreshChannelsScheduledTask>();
_taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>();
return tunerChannelMappings.First(i => string.Equals(i.Id, tunerChannelId, StringComparison.OrdinalIgnoreCase));
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;

View File

@@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.LiveTv;
@@ -10,34 +9,55 @@ using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.LiveTv
{
public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
/// <summary>
/// The "Refresh Guide" scheduled task.
/// </summary>
public class RefreshGuideScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly ILiveTvManager _liveTvManager;
private readonly IConfigurationManager _config;
public RefreshChannelsScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
/// <summary>
/// Initializes a new instance of the <see cref="RefreshGuideScheduledTask"/> class.
/// </summary>
/// <param name="liveTvManager">The live tv manager.</param>
/// <param name="config">The configuration manager.</param>
public RefreshGuideScheduledTask(ILiveTvManager liveTvManager, IConfigurationManager config)
{
_liveTvManager = liveTvManager;
_config = config;
}
/// <inheritdoc />
public string Name => "Refresh Guide";
/// <inheritdoc />
public string Description => "Downloads channel information from live tv services.";
/// <inheritdoc />
public string Category => "Live TV";
public Task Execute(System.Threading.CancellationToken cancellationToken, IProgress<double> progress)
/// <inheritdoc />
public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <inheritdoc />
public string Key => "RefreshGuide";
/// <inheritdoc />
public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
var manager = (LiveTvManager)_liveTvManager;
return manager.RefreshChannels(progress, cancellationToken);
}
/// <summary>
/// Creates the triggers that define when the task will run.
/// </summary>
/// <returns>IEnumerable{BaseTaskTrigger}.</returns>
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
@@ -51,13 +71,5 @@ namespace Emby.Server.Implementations.LiveTv
{
return _config.GetConfiguration<LiveTvOptions>("livetv");
}
public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0;
public bool IsEnabled => true;
public bool IsLogged => true;
public string Key => "RefreshGuide";
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -38,6 +40,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public virtual bool IsSupported => true;
protected abstract Task<List<ChannelInfo>> GetChannelsInternal(TunerHostInfo tuner, CancellationToken cancellationToken);
public abstract string Type { get; }
public async Task<List<ChannelInfo>> GetChannels(TunerHostInfo tuner, bool enableCache, CancellationToken cancellationToken)

View File

@@ -1,3 +1,5 @@
#nullable disable
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
internal class Channels

View File

@@ -1,3 +1,5 @@
#nullable disable
using System;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -8,10 +10,8 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Common.Net;
@@ -19,7 +19,6 @@ using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
@@ -61,7 +60,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_networkManager = networkManager;
_streamHelper = streamHelper;
_jsonOptions = JsonDefaults.GetOptions();
_jsonOptions = JsonDefaults.Options;
}
public string Name => "HD Homerun";
@@ -77,7 +76,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
@@ -185,16 +184,16 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>();
while (!sr.EndOfStream)
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{
string line = StripXML(sr.ReadLine());
if (line.Contains("Channel", StringComparison.Ordinal))
string stripedLine = StripXML(line);
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
{
LiveTvTunerStatus status;
var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = line.Substring(0, index - 1);
var currentChannel = line.Substring(index + 7);
if (currentChannel != "none")
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
var name = stripedLine.Substring(0, index - 1);
var currentChannel = stripedLine.Substring(index + 7);
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
{
status = LiveTvTunerStatus.LiveTv;
}
@@ -335,11 +334,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return new Uri(url).AbsoluteUri.TrimEnd('/');
}
protected EncodingOptions GetEncodingOptions()
{
return Config.GetConfiguration<EncodingOptions>("encoding");
}
private static string GetHdHrIdFromChannelId(string channelId)
{
return channelId.Split('_')[1];
@@ -429,10 +423,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
string audioCodec = channelInfo.AudioCodec;
if (!videoBitrate.HasValue)
{
videoBitrate = isHd ? 15000000 : 2000000;
}
videoBitrate ??= isHd ? 15000000 : 2000000;
int? audioBitrate = isHd ? 448000 : 192000;
@@ -592,7 +583,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
_networkManager,
_streamHelper);
}
@@ -633,7 +623,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
Logger,
Config,
_appHost,
_networkManager,
_streamHelper);
}
@@ -669,7 +658,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_modelCache.Clear();
}
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(new CancellationTokenSource(discoveryDurationMs).Token, cancellationToken).Token;
using var timedCancellationToken = new CancellationTokenSource(discoveryDurationMs);
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timedCancellationToken.Token, cancellationToken);
cancellationToken = linkedCancellationTokenSource.Token;
var list = new List<TunerHostInfo>();
// Create udp broadcast discovery message

View File

@@ -1,7 +1,10 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
@@ -10,6 +13,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common;
using MediaBrowser.Controller.LiveTv;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -26,7 +30,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
public LegacyHdHomerunChannelCommands(string url)
{
// parse url for channel and program
var regExp = new Regex(@"\/ch(\d+)-?(\d*)");
var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
var match = regExp.Match(url);
if (match.Success)
{
@@ -120,17 +124,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken)
{
var lockkeyMsg = CreateGetMessage(tuner, "lockkey");
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
var msgLen = WriteGetMessage(buffer, tuner, "lockkey");
await stream.WriteAsync(buffer.AsMemory(0, msgLen), cancellationToken).ConfigureAwait(false);
ParseReturnMessage(buffer, receivedBytes, out string returnVal);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase);
return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
}
finally
{
@@ -166,24 +168,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_activeTuner = i;
var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue);
var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null);
await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
var lockkeyMsgLen = WriteSetMessage(buffer, i, "lockkey", lockKeyString, null);
await stream.WriteAsync(buffer.AsMemory(0, lockkeyMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
continue;
}
foreach (var command in commands.GetCommands())
{
var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
var channelMsgLen = WriteSetMessage(buffer, i, command.Item1, command.Item2, lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -191,13 +193,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue);
var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);
receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false);
continue;
@@ -232,12 +234,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
foreach (var command in commandList)
{
var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
var channelMsgLen = WriteSetMessage(buffer, _activeTuner, command.Item1, command.Item2, _lockkey);
await stream.WriteAsync(buffer.AsMemory(0, channelMsgLen), cancellationToken).ConfigureAwait(false);
int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
// parse response to make sure it worked
if (!ParseReturnMessage(buffer, receivedBytes, out _))
if (!TryGetReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), out _))
{
return;
}
@@ -265,17 +267,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
var stream = client.GetStream();
var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue);
await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length).ConfigureAwait(false);
var buffer = ArrayPool<byte>.Shared.Rent(8192);
try
{
await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue);
var releaseTargetLen = WriteSetMessage(buffer, _activeTuner, "target", "none", lockKeyValue);
await stream.WriteAsync(buffer.AsMemory(0, releaseTargetLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer).ConfigureAwait(false);
var releaseKeyMsgLen = WriteSetMessage(buffer, _activeTuner, "lockkey", "none", lockKeyValue);
_lockkey = null;
await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length).ConfigureAwait(false);
await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
await stream.WriteAsync(buffer.AsMemory(0, releaseKeyMsgLen)).ConfigureAwait(false);
await stream.ReadAsync(buffer).ConfigureAwait(false);
}
finally
{
@@ -283,249 +285,136 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
}
private static byte[] CreateGetMessage(int tuner, string name)
internal static int WriteGetMessage(Span<byte> buffer, int tuner, string name)
{
var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
int messageLength = byteName.Length + 10; // 4 bytes for header + 4 bytes for crc + 2 bytes for tag name and length
var message = new byte[messageLength];
int offset = InsertHeaderAndName(byteName, messageLength, message);
bool flipEndian = BitConverter.IsLittleEndian;
// calculate crc and insert at the end of the message
var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
if (flipEndian)
{
Array.Reverse(crcBytes);
}
Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
return message;
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int offset = WriteHeaderAndPayload(buffer, byteName);
return FinishPacket(buffer, offset);
}
private static byte[] CreateSetMessage(int tuner, string name, string value, uint? lockkey)
internal static int WriteSetMessage(Span<byte> buffer, int tuner, string name, string value, uint? lockkey)
{
var byteName = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}\0", tuner, name));
var byteValue = Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}\0", value));
var byteName = string.Format(CultureInfo.InvariantCulture, "/tuner{0}/{1}", tuner, name);
int offset = WriteHeaderAndPayload(buffer, byteName);
buffer[offset++] = GetSetValue;
offset += WriteNullTerminatedString(buffer.Slice(offset), value);
int messageLength = byteName.Length + byteValue.Length + 12;
if (lockkey.HasValue)
{
messageLength += 6;
}
var message = new byte[messageLength];
int offset = InsertHeaderAndName(byteName, messageLength, message);
bool flipEndian = BitConverter.IsLittleEndian;
message[offset++] = GetSetValue;
message[offset++] = Convert.ToByte(byteValue.Length);
Buffer.BlockCopy(byteValue, 0, message, offset, byteValue.Length);
offset += byteValue.Length;
if (lockkey.HasValue)
{
message[offset++] = GetSetLockkey;
message[offset++] = 4;
var lockKeyBytes = BitConverter.GetBytes(lockkey.Value);
if (flipEndian)
{
Array.Reverse(lockKeyBytes);
}
Buffer.BlockCopy(lockKeyBytes, 0, message, offset, 4);
buffer[offset++] = GetSetLockkey;
buffer[offset++] = 4;
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset), lockkey.Value);
offset += 4;
}
// calculate crc and insert at the end of the message
var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4));
if (flipEndian)
{
Array.Reverse(crcBytes);
}
Buffer.BlockCopy(crcBytes, 0, message, offset, 4);
return message;
return FinishPacket(buffer, offset);
}
private static int InsertHeaderAndName(byte[] byteName, int messageLength, byte[] message)
internal static int WriteNullTerminatedString(Span<byte> buffer, ReadOnlySpan<char> payload)
{
// check to see if we need to flip endiannes
bool flipEndian = BitConverter.IsLittleEndian;
int offset = 0;
int len = Encoding.UTF8.GetBytes(payload, buffer.Slice(1)) + 1;
// create header bytes
var getSetBytes = BitConverter.GetBytes(GetSetRequest);
var msgLenBytes = BitConverter.GetBytes((ushort)(messageLength - 8)); // Subtrace 4 bytes for header and 4 bytes for crc
// TODO: variable length: this can be 2 bytes if len > 127
// Write length in front of value
buffer[0] = Convert.ToByte(len);
if (flipEndian)
{
Array.Reverse(getSetBytes);
Array.Reverse(msgLenBytes);
}
// null-terminate
buffer[len++] = 0;
// insert header bytes into message
Buffer.BlockCopy(getSetBytes, 0, message, offset, 2);
offset += 2;
Buffer.BlockCopy(msgLenBytes, 0, message, offset, 2);
offset += 2;
return len;
}
// insert tag name and length
message[offset++] = GetSetName;
message[offset++] = Convert.ToByte(byteName.Length);
private static int WriteHeaderAndPayload(Span<byte> buffer, ReadOnlySpan<char> payload)
{
// Packet type
BinaryPrimitives.WriteUInt16BigEndian(buffer, GetSetRequest);
// insert name string
Buffer.BlockCopy(byteName, 0, message, offset, byteName.Length);
offset += byteName.Length;
// We write the payload length at the end
int offset = 4;
// Tag
buffer[offset++] = GetSetName;
// Payload length + data
int strLen = WriteNullTerminatedString(buffer.Slice(offset), payload);
offset += strLen;
return offset;
}
private static bool ParseReturnMessage(byte[] buf, int numBytes, out string returnVal)
private static int FinishPacket(Span<byte> buffer, int offset)
{
returnVal = string.Empty;
// Payload length
BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)(offset - 4));
if (numBytes < 4)
{
return false;
}
// calculate crc and insert at the end of the message
var crc = Crc32.Compute(buffer.Slice(0, offset));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset), crc);
var flipEndian = BitConverter.IsLittleEndian;
int offset = 0;
byte[] msgTypeBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgTypeBytes, 0, msgTypeBytes.Length);
if (flipEndian)
{
Array.Reverse(msgTypeBytes);
}
var msgType = BitConverter.ToUInt16(msgTypeBytes, 0);
offset += 2;
if (msgType != GetSetReply)
{
return false;
}
byte[] msgLengthBytes = new byte[2];
Buffer.BlockCopy(buf, offset, msgLengthBytes, 0, msgLengthBytes.Length);
if (flipEndian)
{
Array.Reverse(msgLengthBytes);
}
var msgLength = BitConverter.ToUInt16(msgLengthBytes, 0);
offset += 2;
if (numBytes < msgLength + 8)
{
return false;
}
offset++; // Name Tag
var nameLength = buf[offset++];
// skip the name field to get to value for return
offset += nameLength;
offset++; // Value Tag
var valueLength = buf[offset++];
returnVal = Encoding.UTF8.GetString(buf, offset, valueLength - 1); // remove null terminator
return true;
return offset + 4;
}
private static class HdHomerunCrc
internal static bool VerifyReturnValueOfGetSet(ReadOnlySpan<byte> buffer, string expected)
{
private static uint[] crc_table = {
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d };
return TryGetReturnValueOfGetSet(buffer, out var value)
&& string.Equals(Encoding.UTF8.GetString(value), expected, StringComparison.OrdinalIgnoreCase);
}
public static uint GetCrc32(byte[] bytes, int numBytes)
internal static bool TryGetReturnValueOfGetSet(ReadOnlySpan<byte> buffer, out ReadOnlySpan<byte> value)
{
value = ReadOnlySpan<byte>.Empty;
if (buffer.Length < 8)
{
var hash = 0xffffffff;
for (var i = 0; i < numBytes; i++)
{
hash = (hash >> 8) ^ crc_table[(hash ^ bytes[i]) & 0xff];
}
var tmp = ~hash & 0xffffffff;
var b0 = tmp & 0xff;
var b1 = (tmp >> 8) & 0xff;
var b2 = (tmp >> 16) & 0xff;
var b3 = (tmp >> 24) & 0xff;
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
return false;
}
uint crc = BinaryPrimitives.ReadUInt32LittleEndian(buffer[^4..]);
if (crc != Crc32.Compute(buffer[..^4]))
{
return false;
}
if (BinaryPrimitives.ReadUInt16BigEndian(buffer) != GetSetReply)
{
return false;
}
var msgLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2));
if (buffer.Length != 2 + 2 + 4 + msgLength)
{
return false;
}
var offset = 4;
if (buffer[offset++] != GetSetName)
{
return false;
}
var nameLength = buffer[offset++];
if (buffer.Length < 4 + 1 + offset + nameLength)
{
return false;
}
offset += nameLength;
if (buffer[offset++] != GetSetValue)
{
return false;
}
var valueLength = buffer[offset++];
if (buffer.Length < 4 + offset + valueLength)
{
return false;
}
// remove null terminator
value = buffer.Slice(offset, valueLength - 1);
return true;
}
}
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -10,7 +12,6 @@ using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Dto;
@@ -28,7 +29,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
private readonly IServerApplicationHost _appHost;
private readonly IHdHomerunChannelCommands _channelCommands;
private readonly int _numTuners;
private readonly INetworkManager _networkManager;
public HdHomerunUdpStream(
MediaSourceInfo mediaSource,
@@ -40,12 +40,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
ILogger logger,
IConfigurationManager configurationManager,
IServerApplicationHost appHost,
INetworkManager networkManager,
IStreamHelper streamHelper)
: base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper)
{
_appHost = appHost;
_networkManager = networkManager;
OriginalStreamId = originalStreamId;
_channelCommands = channelCommands;
_numTuners = numTuners;
@@ -92,7 +90,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
try
{
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false);
await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort, openCancellationToken).ConfigureAwait(false);
localAddress = ((IPEndPoint)tcpClient.Client.LocalEndPoint).Address;
tcpClient.Close();
}
@@ -126,7 +124,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using (udpClient)
using (hdHomerunManager)
{
if (!(ex is OperationCanceledException))
if (ex is not OperationCanceledException)
{
Logger.LogError(ex, "Error opening live stream:");
}

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -150,7 +152,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken)
{
cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token).Token;
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, LiveStreamCancellationTokenSource.Token);
cancellationToken = linkedCancellationTokenSource.Token;
// use non-async filestream on windows along with read due to https://github.com/dotnet/corefx/issues/6039
var allowAsync = Environment.OSVersion.Platform != PlatformID.Win32NT;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -27,6 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
{
private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
".m3u8",
".mpd"
};
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
private readonly INetworkManager _networkManager;
@@ -65,7 +75,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
var channelIdPrefix = GetFullChannelIdPrefix(info);
return await new M3uParser(Logger, _httpClientFactory, _appHost)
return await new M3uParser(Logger, _httpClientFactory)
.Parse(info, channelIdPrefix, cancellationToken)
.ConfigureAwait(false);
}
@@ -86,14 +96,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
".m3u8",
".mpd"
};
protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
{
var tunerCount = info.TunerCount;
@@ -128,7 +130,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public async Task Validate(TunerHostInfo info)
{
using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
using (var stream = await new M3uParser(Logger, _httpClientFactory).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false))
{
}
}

View File

@@ -1,10 +1,11 @@
#nullable disable
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
@@ -20,15 +21,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{
public class M3uParser
{
private const string ExtInfPrefix = "#EXTINF:";
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServerApplicationHost _appHost;
public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost)
public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_appHost = appHost;
}
public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
@@ -36,16 +37,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// Read the file and display it line by line.
using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false)))
{
return GetChannels(reader, channelIdPrefix, info.Id);
}
}
public List<ChannelInfo> ParseString(string text, string channelIdPrefix, string tunerHostId)
{
// Read the file and display it line by line.
using (var reader = new StringReader(text))
{
return GetChannels(reader, channelIdPrefix, tunerHostId);
return await GetChannelsAsync(reader, channelIdPrefix, info.Id).ConfigureAwait(false);
}
}
@@ -69,45 +61,42 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return File.OpenRead(info.Url);
}
private const string ExtInfPrefix = "#EXTINF:";
private List<ChannelInfo> GetChannels(TextReader reader, string channelIdPrefix, string tunerHostId)
private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)
{
var channels = new List<ChannelInfo>();
string line;
string extInf = string.Empty;
while ((line = reader.ReadLine()) != null)
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{
line = line.Trim();
if (string.IsNullOrWhiteSpace(line))
var trimmedLine = line.Trim();
if (string.IsNullOrWhiteSpace(trimmedLine))
{
continue;
}
if (line.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
if (trimmedLine.StartsWith("#EXTM3U", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (line.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
if (trimmedLine.StartsWith(ExtInfPrefix, StringComparison.OrdinalIgnoreCase))
{
extInf = line.Substring(ExtInfPrefix.Length).Trim();
extInf = trimmedLine.Substring(ExtInfPrefix.Length).Trim();
_logger.LogInformation("Found m3u channel: {0}", extInf);
}
else if (!string.IsNullOrWhiteSpace(extInf) && !line.StartsWith('#'))
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, line);
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id))
{
channel.Id = channelIdPrefix + line.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
channel.Path = line;
channel.Path = trimmedLine;
channels.Add(channel);
extInf = string.Empty;
}
@@ -133,6 +122,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
channel.ImageUrl = value;
}
if (attributes.TryGetValue("group-title", out string groupTitle))
{
channel.ChannelGroup = groupTitle;
}
channel.Name = GetChannelName(extInf, attributes);
channel.Number = GetChannelNumber(extInf, attributes, mediaUrl);
@@ -155,7 +149,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (channelIdValues.Count > 0)
{
channel.Id = string.Join("_", channelIdValues);
channel.Id = string.Join('_', channelIdValues);
}
return channel;

View File

@@ -1,3 +1,5 @@
#nullable disable
#pragma warning disable CS1591
using System;
@@ -89,8 +91,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var taskCompletionSource = new TaskCompletionSource<bool>();
var now = DateTime.UtcNow;
_ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
// OpenedMediaSource.Protocol = MediaProtocol.File;
@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
if (!taskCompletionSource.Task.Result)
{
Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
}
}
@@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
});
}, CancellationToken.None);
}
private void Resolve(TaskCompletionSource<bool> openTaskCompletionSource)

View File

@@ -112,5 +112,10 @@
"TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.",
"TaskRefreshLibrary": "Skandeer Media Versameling",
"TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.",
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde"
"TaskRefreshChapterImages": "Verkry Hoofstuk Beelde",
"Undefined": "Ongedefineerd",
"Forced": "Geforseer",
"Default": "Oorspronklik",
"TaskCleanActivityLogDescription": "Verwyder aktiwiteitsaantekeninge ouer as die opgestelde ouderdom.",
"TaskCleanActivityLog": "Maak Aktiwiteitsaantekeninge Skoon"
}

View File

@@ -113,5 +113,10 @@
"TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
"TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.",
"TaskCleanLogs": "حذف دليل السجل"
"TaskCleanLogs": "حذف دليل السجل",
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.",
"TaskCleanActivityLog": "حذف سجل الأنشطة",
"Default": "الإعدادات الافتراضية",
"Undefined": "غير معرف",
"Forced": "ملحقة"
}

View File

@@ -8,7 +8,7 @@
"CameraImageUploadedFrom": "Нова снимка от камера беше качена от {0}",
"Channels": "Канали",
"ChapterNameValue": "Глава {0}",
"Collections": "Колекции",
"Collections": "Поредици",
"DeviceOfflineWithName": "{0} се разкачи",
"DeviceOnlineWithName": "{0} е свързан",
"FailedLoginAttemptWithUserName": "Неуспешен опит за влизане от {0}",
@@ -39,7 +39,7 @@
"MixedContent": "Смесено съдържание",
"Movies": "Филми",
"Music": "Музика",
"MusicVideos": "Музикални клипове",
"MusicVideos": "Музикални видеа",
"NameInstallFailed": "{0} не можа да се инсталира",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Неразпознат сезон",
@@ -55,26 +55,26 @@
"NotificationOptionPluginInstalled": "Приставката е инсталирана",
"NotificationOptionPluginUninstalled": "Приставката е деинсталирана",
"NotificationOptionPluginUpdateInstalled": "Обновлението на приставката е инсталирано",
"NotificationOptionServerRestartRequired": "Нужно е повторно пускане на сървъра",
"NotificationOptionServerRestartRequired": "Сървърът трябва да се рестартира",
"NotificationOptionTaskFailed": "Грешка в планирана задача",
"NotificationOptionUserLockedOut": "Потребителя е заключен",
"NotificationOptionUserLockedOut": "Потребителят е заключен",
"NotificationOptionVideoPlayback": "Възпроизвеждането на видео започна",
"NotificationOptionVideoPlaybackStopped": "Възпроизвеждането на видео е спряно",
"Photos": "Снимки",
"Playlists": "Списъци",
"Plugin": "Приставка",
"PluginInstalledWithName": "{0} е инсталирано",
"PluginUninstalledWithName": "{0} е деинсталирано",
"PluginUpdatedWithName": "{0} е обновено",
"Plugin": "Добавка",
"PluginInstalledWithName": "{0} е инсталиранa",
"PluginUninstalledWithName": "{0} е деинсталиранa",
"PluginUpdatedWithName": "{0} е обновенa",
"ProviderValue": "Доставчик: {0}",
"ScheduledTaskFailedWithName": "{0} се провали",
"ScheduledTaskStartedWithName": "{0} започна",
"ServerNameNeedsToBeRestarted": "{0} е нужно да се рестартира",
"ServerNameNeedsToBeRestarted": "{0} трябва да се рестартира",
"Shows": "Сериали",
"Songs": "Песни",
"StartupEmbyServerIsLoading": "Сървърът зарежда. Моля, опитайте отново след малко.",
"SubtitleDownloadFailureForItem": "Неуспешно изтегляне на субтитри за {0}",
"SubtitleDownloadFailureFromForItem": "Поднадписите за {1} от {0} не можаха да се изтеглят",
"SubtitleDownloadFailureFromForItem": "Субтитрите за {1} от {0} не можаха да бъдат изтеглени",
"Sync": "Синхронизиране",
"System": "Система",
"TvShows": "Телевизионни сериали",
@@ -92,12 +92,12 @@
"ValueHasBeenAddedToLibrary": "{0} беше добавен във Вашата библиотека",
"ValueSpecialEpisodeName": "Специални - {0}",
"VersionNumber": "Версия {0}",
"TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи поднадписи, на база конфигурацията за мета-данни.",
"TaskDownloadMissingSubtitles": "Изтегляне на липсващи поднадписи",
"TaskDownloadMissingSubtitlesDescription": "Търси Интернет за липсващи субтитри, на база конфигурацията за мета-данни.",
"TaskDownloadMissingSubtitles": "Изтегляне на липсващи субтитри",
"TaskRefreshChannelsDescription": "Обновява информацията за интернет канала.",
"TaskRefreshChannels": "Обновяване на Канали",
"TaskCleanTranscodeDescription": "Изтрива прекодирани файлове по-стари от един ден.",
"TaskCleanTranscode": "Изчиства директорията за прекодиране",
"TaskCleanTranscodeDescription": "Изтрива транскодирани файлове по-стари от един ден.",
"TaskCleanTranscode": "Изчиства директорията за транскодиране",
"TaskUpdatePluginsDescription": "Изтегля и инсталира актуализации за добавките, които са настроени за автоматична актуализация.",
"TaskUpdatePlugins": "Актуализира добавките",
"TaskRefreshPeopleDescription": "Актуализира мета-данните за артистите и режисьорите за Вашата медийна библиотека.",
@@ -113,5 +113,10 @@
"TasksChannelsCategory": "Интернет Канали",
"TasksApplicationCategory": "Приложение",
"TasksLibraryCategory": "Библиотека",
"TasksMaintenanceCategory": "Поддръжка"
"TasksMaintenanceCategory": "Поддръжка",
"Undefined": "Неопределено",
"Forced": "Принудително",
"Default": "По подразбиране",
"TaskCleanActivityLogDescription": "Изтрива записите в дневника с активност по стари от конфигурираната възраст.",
"TaskCleanActivityLog": "Изчисти дневника с активност"
}

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