mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-02-14 16:42:24 +00:00
Merge branch 'master' into bug/authorization-header-issue
This commit is contained in:
@@ -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%";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Emby.Server.Implementations.HttpServer;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using DotNet.Globbing;
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
{
|
||||
internal class Channels
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -113,5 +113,10 @@
|
||||
"TaskRefreshPeopleDescription": "تحديث البيانات الوصفية للممثلين والمخرجين في مكتبة الوسائط الخاصة بك.",
|
||||
"TaskRefreshPeople": "إعادة تحميل الأشخاص",
|
||||
"TaskCleanLogsDescription": "حذف السجلات الأقدم من {0} يوم.",
|
||||
"TaskCleanLogs": "حذف دليل السجل"
|
||||
"TaskCleanLogs": "حذف دليل السجل",
|
||||
"TaskCleanActivityLogDescription": "يحذف سجل الأنشطة الأقدم من الوقت الموضوع.",
|
||||
"TaskCleanActivityLog": "حذف سجل الأنشطة",
|
||||
"Default": "الإعدادات الافتراضية",
|
||||
"Undefined": "غير معرف",
|
||||
"Forced": "ملحقة"
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user