mirror of
https://github.com/jellyfin/jellyfin.git
synced 2026-07-19 12:44:19 +01:00
merge branch 'master' into syncplay-clear-queue
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>
|
||||
@@ -297,25 +299,29 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// <inheritdoc />
|
||||
public object GetConfiguration(string key)
|
||||
{
|
||||
return _configurations.GetOrAdd(key, k =>
|
||||
{
|
||||
var file = GetConfigurationFile(key);
|
||||
|
||||
var configurationInfo = _configurationStores
|
||||
.FirstOrDefault(i => string.Equals(i.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (configurationInfo == null)
|
||||
return _configurations.GetOrAdd(
|
||||
key,
|
||||
(k, configurationManager) =>
|
||||
{
|
||||
throw new ResourceNotFoundException("Configuration with key " + key + " not found.");
|
||||
}
|
||||
var file = configurationManager.GetConfigurationFile(k);
|
||||
|
||||
var configurationType = configurationInfo.ConfigurationType;
|
||||
var configurationInfo = Array.Find(
|
||||
configurationManager._configurationStores,
|
||||
i => string.Equals(i.Key, k, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
lock (_configurationSyncLock)
|
||||
{
|
||||
return LoadConfiguration(file, configurationType);
|
||||
}
|
||||
});
|
||||
if (configurationInfo == null)
|
||||
{
|
||||
throw new ResourceNotFoundException("Configuration with key " + k + " not found.");
|
||||
}
|
||||
|
||||
var configurationType = configurationInfo.ConfigurationType;
|
||||
|
||||
lock (configurationManager._configurationSyncLock)
|
||||
{
|
||||
return configurationManager.LoadConfiguration(file, configurationType);
|
||||
}
|
||||
},
|
||||
this);
|
||||
}
|
||||
|
||||
private object LoadConfiguration(string path, Type configurationType)
|
||||
|
||||
@@ -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,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -10,8 +12,6 @@ using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
@@ -38,11 +38,11 @@ using Emby.Server.Implementations.Playlists;
|
||||
using Emby.Server.Implementations.Plugins;
|
||||
using Emby.Server.Implementations.QuickConnect;
|
||||
using Emby.Server.Implementations.ScheduledTasks;
|
||||
using Emby.Server.Implementations.Security;
|
||||
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;
|
||||
@@ -50,7 +50,6 @@ using Jellyfin.Networking.Manager;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
@@ -59,7 +58,6 @@ using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
@@ -75,7 +73,6 @@ using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.QuickConnect;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
@@ -99,10 +96,10 @@ 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;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager;
|
||||
|
||||
namespace Emby.Server.Implementations
|
||||
@@ -117,7 +114,13 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
/// </summary>
|
||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IStartupOptions _startupOptions;
|
||||
private readonly IPluginManager _pluginManager;
|
||||
@@ -126,7 +129,62 @@ namespace Emby.Server.Implementations
|
||||
private IMediaEncoder _mediaEncoder;
|
||||
private ISessionManager _sessionManager;
|
||||
private string[] _urlPrefixes;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
/// </summary>
|
||||
/// <value>All concrete types.</value>
|
||||
private Type[] _allConcreteTypes;
|
||||
|
||||
private DeviceId _deviceId;
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
ApplicationPaths = applicationPaths;
|
||||
LoggerFactory = loggerFactory;
|
||||
_startupOptions = options;
|
||||
_startupConfig = startupConfig;
|
||||
_fileSystemManager = fileSystem;
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
|
||||
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>
|
||||
/// Occurs when [has pending restart changed].
|
||||
/// </summary>
|
||||
public event EventHandler HasPendingRestartChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance can self restart.
|
||||
@@ -149,13 +207,7 @@ namespace Emby.Server.Implementations
|
||||
return false;
|
||||
}
|
||||
|
||||
if (OperatingSystem.Id == OperatingSystemId.Windows
|
||||
|| OperatingSystem.Id == OperatingSystemId.Darwin)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,11 +216,6 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
public INetworkManager NetManager { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [has pending restart changed].
|
||||
/// </summary>
|
||||
public event EventHandler HasPendingRestartChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||
/// </summary>
|
||||
@@ -196,22 +243,11 @@ namespace Emby.Server.Implementations
|
||||
/// <value>The application paths.</value>
|
||||
protected IServerApplicationPaths ApplicationPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
/// </summary>
|
||||
/// <value>All concrete types.</value>
|
||||
private Type[] _allConcreteTypes;
|
||||
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
/// </summary>
|
||||
private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
@@ -229,97 +265,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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||
/// </summary>
|
||||
/// <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="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,
|
||||
IFileSystem fileSystem,
|
||||
IServiceCollection serviceCollection)
|
||||
{
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
ApplicationPaths = applicationPaths;
|
||||
LoggerFactory = loggerFactory;
|
||||
_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>());
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
|
||||
_startupOptions = options;
|
||||
|
||||
// Initialize runtime stat collection
|
||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
||||
{
|
||||
DotNetRuntimeStatsBuilder.Default().StartCollecting();
|
||||
}
|
||||
|
||||
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||
|
||||
_pluginManager = new PluginManager(
|
||||
LoggerFactory.CreateLogger<PluginManager>(),
|
||||
this,
|
||||
ServerConfigurationManager.Configuration,
|
||||
ApplicationPaths.PluginsPath,
|
||||
ApplicationVersion);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
private void MigrateNetworkConfiguration()
|
||||
{
|
||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ServerConfigurationManager.Configuration, networkSettings);
|
||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||
Logger?.LogDebug("Successfully migrated network settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
|
||||
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string ReverseVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
|
||||
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||
|
||||
/// <inheritdoc />
|
||||
public Version ApplicationVersion { get; }
|
||||
@@ -345,16 +293,11 @@ namespace Emby.Server.Implementations
|
||||
/// <value>The application name.</value>
|
||||
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
||||
|
||||
private DeviceId _deviceId;
|
||||
|
||||
public string SystemId
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_deviceId == null)
|
||||
{
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
}
|
||||
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
|
||||
return _deviceId.Value;
|
||||
}
|
||||
@@ -363,6 +306,50 @@ namespace Emby.Server.Implementations
|
||||
/// <inheritdoc/>
|
||||
public string Name => ApplicationProductName;
|
||||
|
||||
private CertificateInfo CertificateInfo { get; set; }
|
||||
|
||||
public X509Certificate2 Certificate { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ListenWithHttps => Certificate != null && ConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||
|
||||
public string FriendlyName =>
|
||||
string.IsNullOrEmpty(ConfigurationManager.Configuration.ServerName)
|
||||
? Environment.MachineName
|
||||
: ConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary function to migration network settings out of system.xml and into network.xml.
|
||||
/// TODO: remove at the point when a fixed migration path has been decided upon.
|
||||
/// </summary>
|
||||
private void MigrateNetworkConfiguration()
|
||||
{
|
||||
string path = Path.Combine(ConfigurationManager.CommonApplicationPaths.ConfigurationDirectoryPath, "network.xml");
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
var networkSettings = new NetworkConfiguration();
|
||||
ClassMigrationHelper.CopyProperties(ConfigurationManager.Configuration, networkSettings);
|
||||
_xmlSerializer.SerializeToFile(networkSettings, path);
|
||||
Logger.LogDebug("Successfully migrated network settings.");
|
||||
}
|
||||
}
|
||||
|
||||
public string ExpandVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
|
||||
return path.Replace(appPaths.VirtualDataPath, appPaths.DataPath, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(appPaths.VirtualInternalMetadataPath, appPaths.InternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public string ReverseVirtualPath(string path)
|
||||
{
|
||||
var appPaths = ApplicationPaths;
|
||||
|
||||
return path.Replace(appPaths.DataPath, appPaths.VirtualDataPath, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of type and resolves all constructor dependencies.
|
||||
/// </summary>
|
||||
@@ -374,7 +361,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);
|
||||
@@ -386,10 +373,7 @@ namespace Emby.Server.Implementations
|
||||
/// <returns>System.Object.</returns>
|
||||
protected object CreateInstanceSafe(Type type)
|
||||
{
|
||||
if (_creatingInstances == null)
|
||||
{
|
||||
_creatingInstances = new List<Type>();
|
||||
}
|
||||
_creatingInstances ??= new List<Type>();
|
||||
|
||||
if (_creatingInstances.IndexOf(type) != -1)
|
||||
{
|
||||
@@ -460,7 +444,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<T> GetExports<T>(CreationDelegate defaultFunc, bool manageLifetime = true)
|
||||
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>()
|
||||
@@ -483,9 +467,11 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Runs the startup tasks.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <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));
|
||||
@@ -495,18 +481,25 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_mediaEncoder.SetFFmpegPath();
|
||||
|
||||
Logger.LogInformation("ServerId: {0}", SystemId);
|
||||
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
||||
|
||||
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();
|
||||
@@ -530,7 +523,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;
|
||||
|
||||
@@ -548,8 +555,6 @@ namespace Emby.Server.Implementations
|
||||
};
|
||||
Certificate = GetCertificate(CertificateInfo);
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
RegisterServices();
|
||||
|
||||
_pluginManager.RegisterServices(ServiceCollection);
|
||||
@@ -564,7 +569,8 @@ 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);
|
||||
@@ -591,8 +597,6 @@ namespace Emby.Server.Implementations
|
||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
|
||||
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
||||
|
||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
@@ -602,14 +606,8 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
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>));
|
||||
@@ -629,8 +627,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||
@@ -666,22 +662,21 @@ namespace Emby.Server.Implementations
|
||||
|
||||
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
|
||||
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||
ServiceCollection.AddScoped<ISessionContext, SessionContext>();
|
||||
|
||||
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
|
||||
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>
|
||||
@@ -696,8 +691,6 @@ namespace Emby.Server.Implementations
|
||||
_mediaEncoder = Resolve<IMediaEncoder>();
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
|
||||
SetStaticProperties();
|
||||
|
||||
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
|
||||
@@ -726,7 +719,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
||||
logger.LogInformation("Operating system: {OS}", OperatingSystem.Name);
|
||||
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
|
||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
||||
@@ -779,7 +772,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>();
|
||||
@@ -801,13 +794,12 @@ 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>());
|
||||
_pluginManager.CreatePlugins();
|
||||
|
||||
_urlPrefixes = GetUrlPrefixes().ToArray();
|
||||
@@ -879,10 +871,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
private CertificateInfo CertificateInfo { get; set; }
|
||||
|
||||
public X509Certificate2 Certificate { get; private set; }
|
||||
|
||||
private IEnumerable<string> GetUrlPrefixes()
|
||||
{
|
||||
var hosts = new[] { "+" };
|
||||
@@ -911,7 +899,7 @@ namespace Emby.Server.Implementations
|
||||
protected void OnConfigurationUpdated(object sender, EventArgs e)
|
||||
{
|
||||
var requiresRestart = false;
|
||||
var networkConfiguration = ServerConfigurationManager.GetNetworkConfiguration();
|
||||
var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
|
||||
|
||||
// Don't do anything if these haven't been set yet
|
||||
if (HttpPort != 0 && HttpsPort != 0)
|
||||
@@ -920,10 +908,10 @@ namespace Emby.Server.Implementations
|
||||
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;
|
||||
}
|
||||
@@ -1104,16 +1092,14 @@ namespace Emby.Server.Implementations
|
||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||
CachePath = ApplicationPaths.CachePath,
|
||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||
OperatingSystemDisplayName = OperatingSystem.Name,
|
||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
|
||||
CanSelfRestart = CanSelfRestart,
|
||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||
HasUpdateAvailable = HasUpdateAvailable,
|
||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||
ServerName = FriendlyName,
|
||||
LocalAddress = GetSmartApiUrl(source),
|
||||
SupportsLibraryMonitor = true,
|
||||
EncoderLocation = _mediaEncoder.EncoderLocation,
|
||||
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
||||
PackageName = _startupOptions.PackageName
|
||||
};
|
||||
@@ -1124,34 +1110,31 @@ namespace Emby.Server.Implementations
|
||||
.Select(i => new WakeOnLanInfo(i))
|
||||
.ToList();
|
||||
|
||||
public PublicSystemInfo GetPublicSystemInfo(IPAddress source)
|
||||
public PublicSystemInfo GetPublicSystemInfo(IPAddress address)
|
||||
{
|
||||
return new PublicSystemInfo
|
||||
{
|
||||
Version = ApplicationVersionString,
|
||||
ProductName = ApplicationProductName,
|
||||
Id = SystemId,
|
||||
OperatingSystem = OperatingSystem.Id.ToString(),
|
||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
||||
ServerName = FriendlyName,
|
||||
LocalAddress = GetSmartApiUrl(source),
|
||||
LocalAddress = GetSmartApiUrl(address),
|
||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.GetNetworkConfiguration().EnableHttps;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr, 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);
|
||||
string smart = NetManager.GetBindInterface(remoteAddr, out port);
|
||||
// If the smartAPI doesn't start with http then treat it as a host or ip.
|
||||
if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -1165,10 +1148,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);
|
||||
@@ -1185,10 +1168,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);
|
||||
@@ -1214,27 +1197,20 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetLocalApiUrl(string host, string scheme = null, int? port = null)
|
||||
public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
|
||||
{
|
||||
// NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
|
||||
// not. For consistency, always trim the trailing slash.
|
||||
return new UriBuilder
|
||||
{
|
||||
Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
|
||||
Host = host,
|
||||
Host = hostname,
|
||||
Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
|
||||
Path = ServerConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
Path = ConfigurationManager.GetNetworkConfiguration().BaseUrl
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
|
||||
public string FriendlyName =>
|
||||
string.IsNullOrEmpty(ServerConfigurationManager.Configuration.ServerName)
|
||||
? Environment.MachineName
|
||||
: ServerConfigurationManager.Configuration.ServerName;
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public async Task Shutdown()
|
||||
{
|
||||
if (IsShuttingDown)
|
||||
@@ -1258,26 +1234,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
protected abstract void ShutdownInternal();
|
||||
|
||||
public event EventHandler HasUpdateAvailableChanged;
|
||||
|
||||
private bool _hasUpdateAvailable;
|
||||
|
||||
public bool HasUpdateAvailable
|
||||
{
|
||||
get => _hasUpdateAvailable;
|
||||
set
|
||||
{
|
||||
var fireEvent = value && !_hasUpdateAvailable;
|
||||
|
||||
_hasUpdateAvailable = value;
|
||||
|
||||
if (fireEvent)
|
||||
{
|
||||
HasUpdateAvailableChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||
{
|
||||
var assemblies = _allConcreteTypes
|
||||
@@ -1292,41 +1248,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void LaunchUrl(string url)
|
||||
{
|
||||
if (!CanLaunchWebBrowser)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
process.Exited += (sender, args) => ((Process)sender).Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error launching url: {url}", url);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
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 Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
@@ -49,7 +50,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChannelManager"/> class.
|
||||
@@ -101,7 +102,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
|
||||
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
|
||||
|
||||
return !(channel is IDisableMediaSourceDisplay);
|
||||
return channel is not IDisableMediaSourceDisplay;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -336,19 +337,19 @@ 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
|
||||
{
|
||||
var jsonString = File.ReadAllText(path, Encoding.UTF8);
|
||||
return JsonSerializer.Deserialize<List<MediaSourceInfo>>(jsonString, _jsonOptions)
|
||||
?? 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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -879,7 +880,7 @@ namespace Emby.Server.Implementations.Channels
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CacheResponse(object result, string path)
|
||||
private async Task CacheResponse(ChannelItemResult result, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1078,11 +1079,11 @@ namespace Emby.Server.Implementations.Channels
|
||||
|
||||
// was used for status
|
||||
// if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
|
||||
//{
|
||||
// {
|
||||
// item.ExternalEtag = info.Etag;
|
||||
// forceUpdate = true;
|
||||
// _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
|
||||
//}
|
||||
// }
|
||||
|
||||
if (!internalChannelId.Equals(item.ChannelId))
|
||||
{
|
||||
|
||||
@@ -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,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -8,11 +7,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;
|
||||
@@ -64,13 +61,13 @@ namespace Emby.Server.Implementations.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
|
||||
public event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
|
||||
public event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
|
||||
public event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
|
||||
|
||||
private IEnumerable<Folder> FindFolders(string path)
|
||||
{
|
||||
@@ -81,14 +78,12 @@ namespace Emby.Server.Implementations.Collections
|
||||
.Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path));
|
||||
}
|
||||
|
||||
internal async Task<Folder> EnsureLibraryFolder(string path, bool createIfNeeded)
|
||||
internal async Task<Folder?> EnsureLibraryFolder(string path, bool createIfNeeded)
|
||||
{
|
||||
var existingFolders = FindFolders(path)
|
||||
.ToList();
|
||||
|
||||
if (existingFolders.Count > 0)
|
||||
var existingFolder = FindFolders(path).FirstOrDefault();
|
||||
if (existingFolder != null)
|
||||
{
|
||||
return existingFolders[0];
|
||||
return existingFolder;
|
||||
}
|
||||
|
||||
if (!createIfNeeded)
|
||||
@@ -100,14 +95,14 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
PathInfos = new[] { new MediaPathInfo { Path = path } },
|
||||
PathInfos = new[] { new MediaPathInfo(path) },
|
||||
EnableRealtimeMonitor = false,
|
||||
SaveLocalMetadata = true
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -117,14 +112,14 @@ namespace Emby.Server.Implementations.Collections
|
||||
return Path.Combine(_appPaths.DataPath, "collections");
|
||||
}
|
||||
|
||||
private Task<Folder> GetCollectionsFolder(bool createIfNeeded)
|
||||
private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
|
||||
{
|
||||
return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
|
||||
}
|
||||
|
||||
private IEnumerable<BoxSet> GetCollections(User user)
|
||||
{
|
||||
var folder = GetCollectionsFolder(false).Result;
|
||||
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||
|
||||
return folder == null
|
||||
? Enumerable.Empty<BoxSet>()
|
||||
@@ -165,9 +160,9 @@ namespace Emby.Server.Implementations.Collections
|
||||
DateCreated = DateTime.UtcNow
|
||||
};
|
||||
|
||||
parentFolder.AddChild(collection, CancellationToken.None);
|
||||
parentFolder.AddChild(collection);
|
||||
|
||||
if (options.ItemIdList.Length > 0)
|
||||
if (options.ItemIdList.Count > 0)
|
||||
{
|
||||
await AddToCollectionAsync(
|
||||
collection.Id,
|
||||
@@ -201,13 +196,12 @@ namespace Emby.Server.Implementations.Collections
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
|
||||
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||
=> AddToCollectionAsync(collectionId, itemIds, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||
|
||||
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
||||
{
|
||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
||||
if (collection == null)
|
||||
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||
{
|
||||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
}
|
||||
@@ -251,11 +245,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
|
||||
if (fireEvent)
|
||||
{
|
||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs
|
||||
{
|
||||
Collection = collection,
|
||||
ItemsChanged = itemList
|
||||
});
|
||||
ItemsAddedToCollection?.Invoke(this, new CollectionModifiedEventArgs(collection, itemList));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,9 +253,7 @@ namespace Emby.Server.Implementations.Collections
|
||||
/// <inheritdoc />
|
||||
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||
{
|
||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
||||
|
||||
if (collection == null)
|
||||
if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
|
||||
{
|
||||
throw new ArgumentException("No collection exists with the supplied Id");
|
||||
}
|
||||
@@ -307,11 +295,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,34 +303,57 @@ 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))
|
||||
{
|
||||
results[item.Id] = item;
|
||||
}
|
||||
else
|
||||
if (item is ISupportsBoxSetGrouping)
|
||||
{
|
||||
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;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
results[item.Id] = item;
|
||||
}
|
||||
|
||||
return results.Values;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
@@ -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;
|
||||
@@ -59,7 +61,7 @@ namespace Emby.Server.Implementations.Data
|
||||
protected virtual int? CacheSize => null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />
|
||||
/// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
|
||||
/// </summary>
|
||||
/// <value>The journal mode.</value>
|
||||
protected virtual string JournalMode => "TRUNCATE";
|
||||
@@ -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)",
|
||||
|
||||
@@ -127,19 +129,17 @@ namespace Emby.Server.Implementations.Data
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves the user data.
|
||||
/// </summary>
|
||||
public void SaveUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
/// <inheritdoc />
|
||||
public void SaveUserData(long userId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
if (userData == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userData));
|
||||
}
|
||||
|
||||
if (internalUserId <= 0)
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(internalUserId));
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
@@ -147,22 +147,23 @@ namespace Emby.Server.Implementations.Data
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
PersistUserData(internalUserId, key, userData, cancellationToken);
|
||||
PersistUserData(userId, key, userData, cancellationToken);
|
||||
}
|
||||
|
||||
public void SaveAllUserData(long internalUserId, UserItemData[] userData, CancellationToken cancellationToken)
|
||||
/// <inheritdoc />
|
||||
public void SaveAllUserData(long userId, UserItemData[] userData, CancellationToken cancellationToken)
|
||||
{
|
||||
if (userData == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(userData));
|
||||
}
|
||||
|
||||
if (internalUserId <= 0)
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(internalUserId));
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
PersistAllUserData(internalUserId, userData, cancellationToken);
|
||||
PersistAllUserData(userId, userData, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,7 +173,6 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="userData">The user data.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public void PersistUserData(long internalUserId, string key, UserItemData userData, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -262,19 +262,19 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <summary>
|
||||
/// Gets the user data.
|
||||
/// </summary>
|
||||
/// <param name="internalUserId">The user id.</param>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>Task{UserItemData}.</returns>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// userId
|
||||
/// or
|
||||
/// key
|
||||
/// key.
|
||||
/// </exception>
|
||||
public UserItemData GetUserData(long internalUserId, string key)
|
||||
public UserItemData GetUserData(long userId, string key)
|
||||
{
|
||||
if (internalUserId <= 0)
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(internalUserId));
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(key))
|
||||
@@ -286,7 +286,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", internalUserId);
|
||||
statement.TryBind("@UserId", userId);
|
||||
statement.TryBind("@Key", key);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
@@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Data
|
||||
}
|
||||
}
|
||||
|
||||
public UserItemData GetUserData(long internalUserId, List<string> keys)
|
||||
public UserItemData GetUserData(long userId, List<string> keys)
|
||||
{
|
||||
if (keys == null)
|
||||
{
|
||||
@@ -311,19 +311,19 @@ namespace Emby.Server.Implementations.Data
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetUserData(internalUserId, keys[0]);
|
||||
return GetUserData(userId, keys[0]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return all user-data associated with the given user.
|
||||
/// </summary>
|
||||
/// <param name="internalUserId"></param>
|
||||
/// <returns></returns>
|
||||
public List<UserItemData> GetAllUserData(long internalUserId)
|
||||
/// <param name="userId">The internal user id.</param>
|
||||
/// <returns>The list of user item data.</returns>
|
||||
public List<UserItemData> GetAllUserData(long userId)
|
||||
{
|
||||
if (internalUserId <= 0)
|
||||
if (userId <= 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(internalUserId));
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
var list = new List<UserItemData>();
|
||||
@@ -332,7 +332,7 @@ namespace Emby.Server.Implementations.Data
|
||||
{
|
||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where userId=@UserId"))
|
||||
{
|
||||
statement.TryBind("@UserId", internalUserId);
|
||||
statement.TryBind("@UserId", userId);
|
||||
|
||||
foreach (var row in statement.ExecuteQuery())
|
||||
{
|
||||
@@ -347,17 +347,18 @@ namespace Emby.Server.Implementations.Data
|
||||
/// <summary>
|
||||
/// Read a row from the specified reader into the provided userData object.
|
||||
/// </summary>
|
||||
/// <param name="reader"></param>
|
||||
private UserItemData ReadRow(IReadOnlyList<IResultSetValue> reader)
|
||||
/// <param name="reader">The list of result set values.</param>
|
||||
/// <returns>The user item data.</returns>
|
||||
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 +366,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,26 +21,16 @@ 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))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(typeName));
|
||||
}
|
||||
|
||||
return _typeMap.GetOrAdd(typeName, LookupType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lookups the type.
|
||||
/// </summary>
|
||||
/// <param name="typeName">Name of the type.</param>
|
||||
/// <returns>Type.</returns>
|
||||
private Type LookupType(string typeName)
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType(typeName))
|
||||
.FirstOrDefault(t => t != null);
|
||||
return _typeMap.GetOrAdd(typeName, k => AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Select(a => a.GetType(k))
|
||||
.FirstOrDefault(t => t != null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
||||
namespace Emby.Server.Implementations.Devices
|
||||
{
|
||||
public class DeviceManager : IDeviceManager
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new ();
|
||||
|
||||
public DeviceManager(IAuthenticationRepository authRepo, IUserManager userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_authRepo = authRepo;
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
||||
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
|
||||
{
|
||||
_capabilitiesMap[deviceId] = capabilities;
|
||||
}
|
||||
|
||||
public void UpdateDeviceOptions(string deviceId, DeviceOptions options)
|
||||
{
|
||||
_authRepo.UpdateDeviceOptions(deviceId, options);
|
||||
|
||||
DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options)));
|
||||
}
|
||||
|
||||
public DeviceOptions GetDeviceOptions(string deviceId)
|
||||
{
|
||||
return _authRepo.GetDeviceOptions(deviceId);
|
||||
}
|
||||
|
||||
public ClientCapabilities GetCapabilities(string id)
|
||||
{
|
||||
return _capabilitiesMap.TryGetValue(id, out ClientCapabilities result)
|
||||
? result
|
||||
: new ClientCapabilities();
|
||||
}
|
||||
|
||||
public DeviceInfo GetDevice(string id)
|
||||
{
|
||||
var session = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
DeviceId = id
|
||||
}).Items.FirstOrDefault();
|
||||
|
||||
var device = session == null ? null : ToDeviceInfo(session);
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
public QueryResult<DeviceInfo> GetDevices(DeviceQuery query)
|
||||
{
|
||||
IEnumerable<AuthenticationInfo> sessions = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
// UserId = query.UserId
|
||||
HasUser = true
|
||||
}).Items;
|
||||
|
||||
// TODO: DeviceQuery doesn't seem to be used from client. Not even Swagger.
|
||||
if (query.SupportsSync.HasValue)
|
||||
{
|
||||
var val = query.SupportsSync.Value;
|
||||
|
||||
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == val);
|
||||
}
|
||||
|
||||
if (!query.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
var user = _userManager.GetUserById(query.UserId);
|
||||
|
||||
sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
|
||||
}
|
||||
|
||||
var array = sessions.Select(ToDeviceInfo).ToArray();
|
||||
|
||||
return new QueryResult<DeviceInfo>(array);
|
||||
}
|
||||
|
||||
private DeviceInfo ToDeviceInfo(AuthenticationInfo authInfo)
|
||||
{
|
||||
var caps = GetCapabilities(authInfo.DeviceId);
|
||||
|
||||
return new DeviceInfo
|
||||
{
|
||||
AppName = authInfo.AppName,
|
||||
AppVersion = authInfo.AppVersion,
|
||||
Id = authInfo.DeviceId,
|
||||
LastUserId = authInfo.UserId,
|
||||
LastUserName = authInfo.UserName,
|
||||
Name = authInfo.DeviceName,
|
||||
DateLastActivity = authInfo.DateLastActivity,
|
||||
IconUrl = caps?.IconUrl
|
||||
};
|
||||
}
|
||||
|
||||
public bool CanAccessDevice(User user, string deviceId)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentException("user not found");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(deviceId));
|
||||
}
|
||||
|
||||
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var capabilities = GetCapabilities(deviceId);
|
||||
|
||||
if (capabilities != null && capabilities.SupportsPersistentIdentifier)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -49,8 +51,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
||||
public DtoService(
|
||||
ILogger<DtoService> logger,
|
||||
ILibraryManager libraryManager,
|
||||
@@ -73,6 +73,8 @@ namespace Emby.Server.Implementations.Dto
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
}
|
||||
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
|
||||
{
|
||||
@@ -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;
|
||||
|
||||
@@ -505,7 +507,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private void AttachPeople(BaseItemDto dto, BaseItem item)
|
||||
{
|
||||
// Ordering by person type to ensure actors and artists are at the front.
|
||||
@@ -614,7 +615,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private void AttachStudios(BaseItemDto dto, BaseItem item)
|
||||
{
|
||||
dto.Studios = item.Studios
|
||||
@@ -665,10 +665,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))
|
||||
{
|
||||
@@ -702,10 +699,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;
|
||||
}
|
||||
@@ -811,7 +805,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
dto.MediaType = item.MediaType;
|
||||
|
||||
if (!(item is LiveTvProgram))
|
||||
if (item is not LiveTvProgram)
|
||||
{
|
||||
dto.LocationType = item.LocationType;
|
||||
}
|
||||
@@ -898,13 +892,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;
|
||||
@@ -935,9 +926,9 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
|
||||
// if (options.ContainsField(ItemFields.MediaSourceCount))
|
||||
//{
|
||||
// {
|
||||
// Songs always have one
|
||||
//}
|
||||
// }
|
||||
}
|
||||
|
||||
if (item is IHasArtist hasArtist)
|
||||
@@ -945,10 +936,10 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.Artists = hasArtist.Artists;
|
||||
|
||||
// var artistItems = _libraryManager.GetArtists(new InternalItemsQuery
|
||||
//{
|
||||
// {
|
||||
// EnableTotalRecordCount = false,
|
||||
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
||||
//});
|
||||
// });
|
||||
|
||||
// dto.ArtistItems = artistItems.Items
|
||||
// .Select(i =>
|
||||
@@ -965,7 +956,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
// Include artists that are not in the database yet, e.g., just added via metadata editor
|
||||
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
|
||||
dto.ArtistItems = hasArtist.Artists
|
||||
//.Except(foundArtists, new DistinctNameComparer())
|
||||
// .Except(foundArtists, new DistinctNameComparer())
|
||||
.Select(i =>
|
||||
{
|
||||
// This should not be necessary but we're seeing some cases of it
|
||||
@@ -997,10 +988,10 @@ namespace Emby.Server.Implementations.Dto
|
||||
dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault();
|
||||
|
||||
// var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery
|
||||
//{
|
||||
// {
|
||||
// EnableTotalRecordCount = false,
|
||||
// ItemIds = new[] { item.Id.ToString("N", CultureInfo.InvariantCulture) }
|
||||
//});
|
||||
// });
|
||||
|
||||
// dto.AlbumArtists = artistItems.Items
|
||||
// .Select(i =>
|
||||
@@ -1015,7 +1006,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
// .ToList();
|
||||
|
||||
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
|
||||
//.Except(foundArtists, new DistinctNameComparer())
|
||||
// .Except(foundArtists, new DistinctNameComparer())
|
||||
.Select(i =>
|
||||
{
|
||||
// This should not be necessary but we're seeing some cases of it
|
||||
@@ -1042,8 +1033,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
|
||||
// Add video info
|
||||
var video = item as Video;
|
||||
if (video != null)
|
||||
if (item is Video video)
|
||||
{
|
||||
dto.VideoType = video.VideoType;
|
||||
dto.Video3DFormat = video.Video3DFormat;
|
||||
@@ -1082,9 +1072,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
if (options.ContainsField(ItemFields.MediaStreams))
|
||||
{
|
||||
// Add VideoInfo
|
||||
var iHasMediaSources = item as IHasMediaSources;
|
||||
|
||||
if (iHasMediaSources != null)
|
||||
if (item is IHasMediaSources)
|
||||
{
|
||||
MediaStream[] mediaStreams;
|
||||
|
||||
@@ -1153,11 +1141,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
// TODO maybe remove the if statement entirely
|
||||
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
||||
{
|
||||
episodeSeries = episodeSeries ?? episode.Series;
|
||||
episodeSeries ??= episode.Series;
|
||||
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);
|
||||
}
|
||||
@@ -1166,7 +1154,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.SeriesStudio))
|
||||
{
|
||||
episodeSeries = episodeSeries ?? episode.Series;
|
||||
episodeSeries ??= episode.Series;
|
||||
if (episodeSeries != null)
|
||||
{
|
||||
dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault();
|
||||
@@ -1179,7 +1167,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
{
|
||||
dto.AirDays = series.AirDays;
|
||||
dto.AirTime = series.AirTime;
|
||||
dto.Status = series.Status.HasValue ? series.Status.Value.ToString() : null;
|
||||
dto.Status = series.Status?.ToString();
|
||||
}
|
||||
|
||||
// Add SeasonInfo
|
||||
@@ -1192,7 +1180,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
if (options.ContainsField(ItemFields.SeriesStudio))
|
||||
{
|
||||
series = series ?? season.Series;
|
||||
series ??= season.Series;
|
||||
if (series != null)
|
||||
{
|
||||
dto.SeriesStudio = series.Studios.FirstOrDefault();
|
||||
@@ -1203,11 +1191,11 @@ namespace Emby.Server.Implementations.Dto
|
||||
// TODO maybe remove the if statement entirely
|
||||
// if (options.ContainsField(ItemFields.SeriesPrimaryImage))
|
||||
{
|
||||
series = series ?? season.Series;
|
||||
series ??= season.Series;
|
||||
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);
|
||||
}
|
||||
@@ -1290,7 +1278,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
var parent = currentItem.DisplayParent ?? currentItem.GetOwner() ?? currentItem.GetParent();
|
||||
|
||||
if (parent == null && !(originalItem is UserRootFolder) && !(originalItem is UserView) && !(originalItem is AggregateFolder) && !(originalItem is ICollectionFolder) && !(originalItem is Channel))
|
||||
if (parent == null && originalItem is not UserRootFolder && originalItem is not UserView && originalItem is not AggregateFolder && originalItem is not ICollectionFolder && originalItem is not Channel)
|
||||
{
|
||||
parent = _libraryManager.GetCollectionFolders(originalItem).FirstOrDefault();
|
||||
}
|
||||
@@ -1323,9 +1311,12 @@ namespace Emby.Server.Implementations.Dto
|
||||
|
||||
var imageTags = dto.ImageTags;
|
||||
|
||||
while (((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0) || (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0) || parent is Series) &&
|
||||
(parent = parent ?? (isFirst ? GetImageDisplayParent(item, item) ?? owner : parent)) != null)
|
||||
while ((!(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && logoLimit > 0)
|
||||
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && artLimit > 0)
|
||||
|| (!(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && thumbLimit > 0)
|
||||
|| parent is Series)
|
||||
{
|
||||
parent ??= isFirst ? GetImageDisplayParent(item, item) ?? owner : parent;
|
||||
if (parent == null)
|
||||
{
|
||||
break;
|
||||
@@ -1355,7 +1346,7 @@ namespace Emby.Server.Implementations.Dto
|
||||
}
|
||||
}
|
||||
|
||||
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView))
|
||||
if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView)
|
||||
{
|
||||
var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb);
|
||||
|
||||
@@ -1405,7 +1396,6 @@ namespace Emby.Server.Implementations.Dto
|
||||
/// </summary>
|
||||
/// <param name="dto">The dto.</param>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public void AttachPrimaryImageAspectRatio(IItemDto dto, BaseItem item)
|
||||
{
|
||||
dto.PrimaryImageAspectRatio = GetPrimaryImageAspectRatio(item);
|
||||
|
||||
@@ -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" />
|
||||
@@ -22,15 +23,17 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.4" />
|
||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.2" />
|
||||
<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.9" />
|
||||
<PackageReference Include="Mono.Nat" Version="3.0.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.1" />
|
||||
<PackageReference Include="sharpcompress" Version="0.27.1" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.0" />
|
||||
<PackageReference Include="sharpcompress" Version="0.28.3" />
|
||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -42,23 +45,22 @@
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release'">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</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" />
|
||||
|
||||
@@ -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;
|
||||
@@ -147,7 +149,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
|
||||
private static bool EnableRefreshMessage(BaseItem item)
|
||||
{
|
||||
if (!(item is Folder folder))
|
||||
if (item is not Folder folder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -401,7 +403,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item is IItemByName && !(item is MusicArtist))
|
||||
if (item is IItemByName && item is not MusicArtist)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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,13 +30,16 @@ 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;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param>
|
||||
/// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
|
||||
public UdpServerEntryPoint(
|
||||
ILogger<UdpServerEntryPoint> logger,
|
||||
IServerApplicationHost appHost,
|
||||
@@ -49,10 +53,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 +68,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 +85,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,6 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Net;
|
||||
@@ -18,9 +18,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
_authorizationContext = authorizationContext;
|
||||
}
|
||||
|
||||
public AuthorizationInfo Authenticate(HttpRequest request)
|
||||
public async Task<AuthorizationInfo> Authenticate(HttpRequest request)
|
||||
{
|
||||
var auth = _authorizationContext.GetAuthorizationInfo(request);
|
||||
var auth = await _authorizationContext.GetAuthorizationInfo(request).ConfigureAwait(false);
|
||||
|
||||
if (!auth.HasToken)
|
||||
{
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer.Security
|
||||
{
|
||||
public class AuthorizationContext : IAuthorizationContext
|
||||
{
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
public AuthorizationContext(IAuthenticationRepository authRepo, IUserManager userManager)
|
||||
{
|
||||
_authRepo = authRepo;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext)
|
||||
{
|
||||
if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached))
|
||||
{
|
||||
return (AuthorizationInfo)cached;
|
||||
}
|
||||
|
||||
return GetAuthorization(requestContext);
|
||||
}
|
||||
|
||||
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(requestContext);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private AuthorizationInfo GetAuthorization(HttpContext httpReq)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var authInfo = GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
private AuthorizationInfo GetAuthorizationInfoFromDictionary(
|
||||
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;
|
||||
|
||||
if (auth != null)
|
||||
{
|
||||
auth.TryGetValue("DeviceId", out deviceId);
|
||||
auth.TryGetValue("Device", out device);
|
||||
auth.TryGetValue("Client", out client);
|
||||
auth.TryGetValue("Version", out version);
|
||||
auth.TryGetValue("Token", out token);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = headers["X-Emby-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = headers["X-MediaBrowser-Token"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["ApiKey"];
|
||||
}
|
||||
|
||||
// TODO deprecate this query parameter.
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = queryString["api_key"];
|
||||
}
|
||||
|
||||
var authInfo = new AuthorizationInfo
|
||||
{
|
||||
Client = client,
|
||||
Device = device,
|
||||
DeviceId = deviceId,
|
||||
Version = version,
|
||||
Token = token,
|
||||
IsAuthenticated = false,
|
||||
HasToken = false
|
||||
};
|
||||
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
// Request doesn't contain a token.
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
authInfo.HasToken = true;
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery
|
||||
{
|
||||
AccessToken = token
|
||||
});
|
||||
|
||||
if (result.Items.Count > 0)
|
||||
{
|
||||
authInfo.IsAuthenticated = true;
|
||||
}
|
||||
|
||||
var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
|
||||
|
||||
if (originalAuthenticationInfo != null)
|
||||
{
|
||||
var updateToken = false;
|
||||
|
||||
// TODO: Remove these checks for IsNullOrWhiteSpace
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Client))
|
||||
{
|
||||
authInfo.Client = originalAuthenticationInfo.AppName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
|
||||
{
|
||||
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
|
||||
}
|
||||
|
||||
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
|
||||
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Device))
|
||||
{
|
||||
authInfo.Device = originalAuthenticationInfo.DeviceName;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.DeviceName = authInfo.Device;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(authInfo.Version))
|
||||
{
|
||||
authInfo.Version = originalAuthenticationInfo.AppVersion;
|
||||
}
|
||||
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (allowTokenInfoUpdate)
|
||||
{
|
||||
updateToken = true;
|
||||
originalAuthenticationInfo.AppVersion = authInfo.Version;
|
||||
}
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
|
||||
{
|
||||
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
|
||||
{
|
||||
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
|
||||
|
||||
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
originalAuthenticationInfo.UserName = authInfo.User.Username;
|
||||
updateToken = true;
|
||||
}
|
||||
|
||||
authInfo.IsApiKey = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
authInfo.IsApiKey = true;
|
||||
}
|
||||
|
||||
if (updateToken)
|
||||
{
|
||||
_authRepo.Update(originalAuthenticationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq)
|
||||
{
|
||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
||||
|
||||
if (string.IsNullOrEmpty(auth))
|
||||
{
|
||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
||||
}
|
||||
|
||||
return GetAuthorization(auth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
|
||||
{
|
||||
var auth = httpReq.Headers["X-Emby-Authorization"];
|
||||
|
||||
if (string.IsNullOrEmpty(auth))
|
||||
{
|
||||
auth = httpReq.Headers[HeaderNames.Authorization];
|
||||
}
|
||||
|
||||
return GetAuthorization(auth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
/// <param name="authorizationHeader">The authorization header.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private Dictionary<string, string> GetAuthorization(string authorizationHeader)
|
||||
{
|
||||
if (authorizationHeader == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = authorizationHeader.Split(' ', 2);
|
||||
|
||||
// There should be at least to parts
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var acceptedNames = new[] { "MediaBrowser", "Emby" };
|
||||
|
||||
// It has to be a digest request
|
||||
if (!acceptedNames.Contains(parts[0], StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove uptil the first space
|
||||
authorizationHeader = parts[1];
|
||||
parts = authorizationHeader.Split(',');
|
||||
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in parts)
|
||||
{
|
||||
var param = item.Trim().Split('=', 2);
|
||||
|
||||
if (param.Length == 2)
|
||||
{
|
||||
var value = NormalizeValue(param[1].Trim('"'));
|
||||
result[param[0]] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeValue(string value)
|
||||
{
|
||||
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -23,27 +24,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public SessionInfo GetSession(HttpContext requestContext)
|
||||
public async Task<SessionInfo> GetSession(HttpContext requestContext)
|
||||
{
|
||||
var authorization = _authContext.GetAuthorizationInfo(requestContext);
|
||||
var authorization = await _authContext.GetAuthorizationInfo(requestContext).ConfigureAwait(false);
|
||||
|
||||
var user = authorization.User;
|
||||
return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user);
|
||||
return await _sessionManager.LogSessionActivity(
|
||||
authorization.Client,
|
||||
authorization.Version,
|
||||
authorization.DeviceId,
|
||||
authorization.Device,
|
||||
requestContext.GetNormalizedRemoteIp().ToString(),
|
||||
user).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public SessionInfo GetSession(object requestContext)
|
||||
public Task<SessionInfo> GetSession(object requestContext)
|
||||
{
|
||||
return GetSession((HttpContext)requestContext);
|
||||
}
|
||||
|
||||
public User GetUser(HttpContext requestContext)
|
||||
public async Task<User?> GetUser(HttpContext requestContext)
|
||||
{
|
||||
var session = GetSession(requestContext);
|
||||
var session = await GetSession(requestContext).ConfigureAwait(false);
|
||||
|
||||
return session == null || session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId);
|
||||
}
|
||||
|
||||
public User GetUser(object requestContext)
|
||||
public Task<User?> GetUser(object requestContext)
|
||||
{
|
||||
return GetUser(((HttpRequest)requestContext).HttpContext);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
#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;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.Session;
|
||||
@@ -55,7 +54,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
QueryString = query;
|
||||
|
||||
_jsonOptions = JsonDefaults.GetOptions();
|
||||
_jsonOptions = JsonDefaults.Options;
|
||||
LastActivityDate = DateTime.Now;
|
||||
}
|
||||
|
||||
@@ -63,7 +62,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
public event EventHandler<EventArgs>? Closed;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the remote end point.
|
||||
/// Gets the remote end point.
|
||||
/// </summary>
|
||||
public IPAddress? RemoteEndPoint { get; }
|
||||
|
||||
@@ -83,7 +82,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
public DateTime LastKeepAliveDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the query string.
|
||||
/// Gets the query string.
|
||||
/// </summary>
|
||||
/// <value>The query string.</value>
|
||||
public IQueryCollection QueryString { get; }
|
||||
@@ -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)
|
||||
{
|
||||
_ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
|
||||
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,16 +2,15 @@
|
||||
|
||||
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 Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
@@ -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 = OperatingSystem.IsWindows();
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -246,16 +243,23 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
result.Length = fileInfo.Length;
|
||||
|
||||
// Issue #2354 get the size of files behind symbolic links
|
||||
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
// Issue #2354 get the size of files behind symbolic links. Also Enum.HasFlag is bad as it boxes!
|
||||
if ((fileInfo.Attributes & FileAttributes.ReparsePoint) == 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>
|
||||
@@ -376,7 +401,7 @@ namespace Emby.Server.Implementations.IO
|
||||
|
||||
public virtual void SetHidden(string path, bool isHidden)
|
||||
{
|
||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -398,9 +423,9 @@ namespace Emby.Server.Implementations.IO
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly)
|
||||
public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
|
||||
{
|
||||
if (OperatingSystem.Id != OperatingSystemId.Windows)
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -412,14 +437,14 @@ namespace Emby.Server.Implementations.IO
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.IsReadOnly == isReadOnly && info.IsHidden == isHidden)
|
||||
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var attributes = File.GetAttributes(path);
|
||||
|
||||
if (isReadOnly)
|
||||
if (readOnly)
|
||||
{
|
||||
attributes = attributes | FileAttributes.ReadOnly;
|
||||
}
|
||||
@@ -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)
|
||||
@@ -590,7 +601,7 @@ 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 enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
@@ -607,13 +618,13 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
files = files.Where(i =>
|
||||
{
|
||||
var ext = i.Extension;
|
||||
if (ext == null)
|
||||
var ext = i.Extension.AsSpan();
|
||||
if (ext.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
||||
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -625,8 +636,7 @@ namespace Emby.Server.Implementations.IO
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
|
||||
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
|
||||
return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", enumerationOptions));
|
||||
}
|
||||
|
||||
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
||||
@@ -644,7 +654,7 @@ 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 enumerationOptions = GetEnumerationOptions(recursive);
|
||||
|
||||
@@ -661,13 +671,13 @@ namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
files = files.Where(i =>
|
||||
{
|
||||
var ext = Path.GetExtension(i);
|
||||
if (ext == null)
|
||||
var ext = Path.GetExtension(i.AsSpan());
|
||||
if (ext.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase);
|
||||
return extensions.Contains(ext, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -684,23 +694,10 @@ namespace Emby.Server.Implementations.IO
|
||||
return new EnumerationOptions
|
||||
{
|
||||
RecurseSubdirectories = recursive,
|
||||
IgnoreInaccessible = true
|
||||
IgnoreInaccessible = true,
|
||||
// Don't skip any files.
|
||||
AttributesToSkip = 0
|
||||
};
|
||||
}
|
||||
|
||||
private static void RunProcess(string path, string args, string workingDirectory)
|
||||
{
|
||||
using (var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
Arguments = args,
|
||||
FileName = path,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = workingDirectory,
|
||||
WindowStyle = ProcessWindowStyle.Normal
|
||||
}))
|
||||
{
|
||||
process.WaitForExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,31 +7,31 @@ 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.
|
||||
/// Gets a value value indicating whether to run as service by the --service command line option.
|
||||
/// </summary>
|
||||
bool IsService { get; }
|
||||
|
||||
/// <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;
|
||||
@@ -49,7 +51,7 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
public int Order => 0;
|
||||
|
||||
protected virtual bool Supports(BaseItem _) => true;
|
||||
protected virtual bool Supports(BaseItem item) => true;
|
||||
|
||||
public async Task<ItemUpdateType> FetchAsync(T item, MetadataRefreshOptions options, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -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;
|
||||
@@ -28,27 +30,27 @@ namespace Emby.Server.Implementations.Images
|
||||
|
||||
string[] includeItemTypes;
|
||||
|
||||
if (string.Equals(viewType, CollectionType.Movies))
|
||||
if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Movie" };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.TvShows))
|
||||
else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Series" };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Music))
|
||||
else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "MusicAlbum" };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.Books))
|
||||
else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Book", "AudioBook" };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.BoxSets))
|
||||
else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "BoxSet" };
|
||||
}
|
||||
else if (string.Equals(viewType, CollectionType.HomeVideos) || string.Equals(viewType, CollectionType.Photos))
|
||||
else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
|
||||
{
|
||||
includeItemTypes = new string[] { "Video", "Photo" };
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.Library
|
||||
if (parent != null)
|
||||
{
|
||||
// Don't resolve these into audio files
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(filename), BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
||||
if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFilename, StringComparison.Ordinal)
|
||||
&& _libraryManager.IsAudioFile(filename))
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -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;
|
||||
@@ -19,6 +21,7 @@ using Emby.Server.Implementations.Playlists;
|
||||
using Emby.Server.Implementations.ScheduledTasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
@@ -48,6 +51,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 +179,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
lock (_rootFolderSyncLock)
|
||||
{
|
||||
if (_rootFolder == null)
|
||||
{
|
||||
_rootFolder = CreateRootFolder();
|
||||
}
|
||||
_rootFolder ??= CreateRootFolder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,33 +197,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; }
|
||||
|
||||
@@ -286,14 +287,14 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
if (item is IItemByName)
|
||||
{
|
||||
if (!(item is MusicArtist))
|
||||
if (item is not MusicArtist)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!item.IsFolder)
|
||||
{
|
||||
if (!(item is Video) && !(item is LiveTvChannel))
|
||||
if (item is not Video && item is not LiveTvChannel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -558,7 +559,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 +684,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));
|
||||
@@ -697,25 +697,32 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> ResolveFileList(
|
||||
IEnumerable<FileSystemMetadata> fileList,
|
||||
IReadOnlyList<FileSystemMetadata> fileList,
|
||||
IDirectoryService directoryService,
|
||||
Folder parent,
|
||||
string collectionType,
|
||||
IItemResolver[] resolvers,
|
||||
LibraryOptions libraryOptions)
|
||||
{
|
||||
return fileList.Select(f =>
|
||||
// Given that fileList is a list we can save enumerator allocations by indexing
|
||||
for (var i = 0; i < fileList.Count; i++)
|
||||
{
|
||||
var file = fileList[i];
|
||||
BaseItem result = null;
|
||||
try
|
||||
{
|
||||
return ResolvePath(f, directoryService, resolvers, parent, collectionType, libraryOptions);
|
||||
result = ResolvePath(file, directoryService, resolvers, parent, collectionType, libraryOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error resolving path {path}", f.FullName);
|
||||
return null;
|
||||
_logger.LogError(ex, "Error resolving path {Path}", file.FullName);
|
||||
}
|
||||
}).Where(i => i != null);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
yield return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -859,7 +866,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Person.GetPath(name);
|
||||
var id = GetItemByNameId<Person>(path);
|
||||
if (!(GetItemById(id) is Person item))
|
||||
if (GetItemById(id) is not Person item)
|
||||
{
|
||||
item = new Person
|
||||
{
|
||||
@@ -1066,17 +1073,17 @@ namespace Emby.Server.Implementations.Library
|
||||
// Start by just validating the children of the root, but go no further
|
||||
await RootFolder.ValidateChildren(
|
||||
new SimpleProgress<double>(),
|
||||
cancellationToken,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||
recursive: false).ConfigureAwait(false);
|
||||
recursive: false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await GetUserRootFolder().ValidateChildren(
|
||||
new SimpleProgress<double>(),
|
||||
cancellationToken,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||
recursive: false).ConfigureAwait(false);
|
||||
recursive: false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Quickly scan CollectionFolders for changes
|
||||
foreach (var folder in GetUserRootFolder().Children.OfType<Folder>())
|
||||
@@ -1096,7 +1103,7 @@ namespace Emby.Server.Implementations.Library
|
||||
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
|
||||
|
||||
// Validate the entire media library
|
||||
await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true).ConfigureAwait(false);
|
||||
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress.Report(96);
|
||||
|
||||
@@ -1163,7 +1170,7 @@ namespace Emby.Server.Implementations.Library
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
_itemRepository.UpdateInheritedValues(cancellationToken);
|
||||
_itemRepository.UpdateInheritedValues();
|
||||
|
||||
progress.Report(100);
|
||||
}
|
||||
@@ -1240,11 +1247,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>
|
||||
@@ -1745,22 +1761,20 @@ namespace Emby.Server.Implementations.Library
|
||||
return orderedItems ?? items;
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderByList)
|
||||
public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ValueTuple<string, SortOrder>> orderBy)
|
||||
{
|
||||
var isFirst = true;
|
||||
|
||||
IOrderedEnumerable<BaseItem> orderedItems = null;
|
||||
|
||||
foreach (var orderBy in orderByList)
|
||||
foreach (var (name, sortOrder) in orderBy)
|
||||
{
|
||||
var comparer = GetComparer(orderBy.Item1, user);
|
||||
var comparer = GetComparer(name, user);
|
||||
if (comparer == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sortOrder = orderBy.Item2;
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
orderedItems = sortOrder == SortOrder.Descending ? items.OrderByDescending(i => i, comparer) : items.OrderBy(i => i, comparer);
|
||||
@@ -1905,12 +1919,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 +1942,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 +1954,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 +1964,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2063,7 +2082,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return new List<Folder>();
|
||||
}
|
||||
|
||||
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>().ToList());
|
||||
return GetCollectionFoldersInternal(item, GetUserRootFolder().Children.OfType<Folder>());
|
||||
}
|
||||
|
||||
public List<Folder> GetCollectionFolders(BaseItem item, List<Folder> allUserRootChildren)
|
||||
@@ -2088,20 +2107,20 @@ namespace Emby.Server.Implementations.Library
|
||||
return GetCollectionFoldersInternal(item, allUserRootChildren);
|
||||
}
|
||||
|
||||
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, List<Folder> allUserRootChildren)
|
||||
private static List<Folder> GetCollectionFoldersInternal(BaseItem item, IEnumerable<Folder> allUserRootChildren)
|
||||
{
|
||||
return allUserRootChildren
|
||||
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path.AsSpan(), StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public LibraryOptions GetLibraryOptions(BaseItem item)
|
||||
{
|
||||
if (!(item is CollectionFolder collectionFolder))
|
||||
if (item is not CollectionFolder collectionFolder)
|
||||
{
|
||||
// List.Find is more performant than FirstOrDefault due to enumerator allocation
|
||||
collectionFolder = GetCollectionFolders(item)
|
||||
.OfType<CollectionFolder>()
|
||||
.FirstOrDefault();
|
||||
.Find(folder => folder is CollectionFolder) as CollectionFolder;
|
||||
}
|
||||
|
||||
return collectionFolder == null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
|
||||
@@ -2487,8 +2506,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// <inheritdoc />
|
||||
public bool IsVideoFile(string path)
|
||||
{
|
||||
var resolver = new VideoResolver(GetNamingOptions());
|
||||
return resolver.IsVideoFile(path);
|
||||
return VideoResolver.IsVideoFile(path, GetNamingOptions());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -2503,7 +2521,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 +2533,24 @@ 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
|
||||
var parent = episode.GetParent();
|
||||
if (episodeInfo == null && parent.GetType() == typeof(Folder))
|
||||
{
|
||||
episodeInfo = resolver.Resolve(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
|
||||
{
|
||||
@@ -2652,6 +2685,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NamingOptions GetNamingOptions()
|
||||
{
|
||||
if (_namingOptions == null)
|
||||
@@ -2665,13 +2699,12 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public ItemLookupInfo ParseName(string name)
|
||||
{
|
||||
var resolver = new VideoResolver(GetNamingOptions());
|
||||
|
||||
var result = resolver.CleanDateTime(name);
|
||||
var namingOptions = GetNamingOptions();
|
||||
var result = VideoResolver.CleanDateTime(name, namingOptions);
|
||||
|
||||
return new ItemLookupInfo
|
||||
{
|
||||
Name = resolver.TryCleanString(result.Name, out var newName) ? newName.ToString() : result.Name,
|
||||
Name = VideoResolver.TryCleanString(result.Name, namingOptions, out var newName) ? newName.ToString() : result.Name,
|
||||
Year = result.Year
|
||||
};
|
||||
}
|
||||
@@ -2685,9 +2718,7 @@ namespace Emby.Server.Implementations.Library
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videoListResolver = new VideoListResolver(namingOptions);
|
||||
|
||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -2731,9 +2762,7 @@ namespace Emby.Server.Implementations.Library
|
||||
.SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
|
||||
.ToList();
|
||||
|
||||
var videoListResolver = new VideoListResolver(namingOptions);
|
||||
|
||||
var videos = videoListResolver.Resolve(fileSystemChildren);
|
||||
var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
|
||||
|
||||
var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
@@ -2767,6 +2796,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||
{
|
||||
string newPath;
|
||||
if (ownerItem != null)
|
||||
{
|
||||
var libraryOptions = GetLibraryOptions(ownerItem);
|
||||
@@ -2774,15 +2804,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 +2815,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 +2833,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 +2895,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 +2908,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 +2945,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 +2979,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 +3013,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(() =>
|
||||
@@ -3033,9 +3074,9 @@ namespace Emby.Server.Implementations.Library
|
||||
});
|
||||
}
|
||||
|
||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||
public void AddMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
|
||||
{
|
||||
AddMediaPathInternal(virtualFolderName, pathInfo, true);
|
||||
AddMediaPathInternal(virtualFolderName, mediaPath, true);
|
||||
}
|
||||
|
||||
private void AddMediaPathInternal(string virtualFolderName, MediaPathInfo pathInfo, bool saveLibraryOptions)
|
||||
@@ -3088,11 +3129,11 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo pathInfo)
|
||||
public void UpdateMediaPath(string virtualFolderName, MediaPathInfo mediaPath)
|
||||
{
|
||||
if (pathInfo == null)
|
||||
if (mediaPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(pathInfo));
|
||||
throw new ArgumentNullException(nameof(mediaPath));
|
||||
}
|
||||
|
||||
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
|
||||
@@ -3105,9 +3146,9 @@ namespace Emby.Server.Implementations.Library
|
||||
var list = libraryOptions.PathInfos.ToList();
|
||||
foreach (var originalPathInfo in list)
|
||||
{
|
||||
if (string.Equals(pathInfo.Path, originalPathInfo.Path, StringComparison.Ordinal))
|
||||
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
|
||||
{
|
||||
originalPathInfo.NetworkPath = pathInfo.NetworkPath;
|
||||
originalPathInfo.NetworkPath = mediaPath.NetworkPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -3130,10 +3171,7 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
if (!list.Any(i => string.Equals(i.Path, location, StringComparison.Ordinal)))
|
||||
{
|
||||
list.Add(new MediaPathInfo
|
||||
{
|
||||
Path = location
|
||||
});
|
||||
list.Add(new MediaPathInfo(location));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -10,7 +12,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
@@ -25,7 +27,7 @@ namespace Emby.Server.Implementations.Library
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
public LiveStreamHelper(IMediaEncoder mediaEncoder, ILogger logger, IApplicationPaths appPaths)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -13,7 +15,7 @@ using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
@@ -46,7 +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.GetOptions();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
|
||||
@@ -199,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)
|
||||
@@ -345,7 +352,7 @@ namespace Emby.Server.Implementations.Library
|
||||
|
||||
private string[] NormalizeLanguage(string language)
|
||||
{
|
||||
if (language == null)
|
||||
if (string.IsNullOrEmpty(language))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
@@ -374,8 +381,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
|
||||
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||
var preferredSubs = NormalizeLanguage(user.SubtitleLanguagePreference);
|
||||
|
||||
var defaultAudioIndex = source.DefaultAudioStreamIndex;
|
||||
var audioLangage = defaultAudioIndex == null
|
||||
@@ -404,9 +410,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
}
|
||||
|
||||
var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
|
||||
? Array.Empty<string>()
|
||||
: NormalizeLanguage(user.AudioLanguagePreference);
|
||||
var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
|
||||
|
||||
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
|
||||
}
|
||||
@@ -436,7 +440,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 =>
|
||||
{
|
||||
@@ -451,8 +455,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();
|
||||
}
|
||||
|
||||
@@ -515,7 +520,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
// TODO: @bond Fix
|
||||
var json = JsonSerializer.Serialize(mediaSource, _jsonOptions);
|
||||
var json = JsonSerializer.SerializeToUtf8Bytes(mediaSource, _jsonOptions);
|
||||
_logger.LogInformation("Live stream opened: " + json);
|
||||
var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
|
||||
|
||||
@@ -584,18 +589,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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -34,9 +36,10 @@ namespace Emby.Server.Implementations.Library
|
||||
return list.Concat(GetInstantMixFromGenres(item.Genres, user, dtoOptions)).ToList();
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromArtist(MusicArtist item, User user, DtoOptions dtoOptions)
|
||||
/// <inheritdoc />
|
||||
public List<BaseItem> GetInstantMixFromArtist(MusicArtist artist, User user, DtoOptions dtoOptions)
|
||||
{
|
||||
return GetInstantMixFromGenres(item.Genres, user, dtoOptions);
|
||||
return GetInstantMixFromGenres(artist.Genres, user, dtoOptions);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetInstantMixFromAlbum(MusicAlbum item, User user, DtoOptions dtoOptions)
|
||||
@@ -100,8 +103,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;
|
||||
@@ -19,11 +21,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
/// </summary>
|
||||
public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
|
||||
{
|
||||
private readonly ILibraryManager LibraryManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
||||
public AudioResolver(ILibraryManager libraryManager)
|
||||
{
|
||||
LibraryManager = libraryManager;
|
||||
_libraryManager = libraryManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,13 +88,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
}
|
||||
|
||||
var files = args.FileSystemChildren
|
||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||
.Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
|
||||
.ToList();
|
||||
|
||||
return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||
}
|
||||
|
||||
if (LibraryManager.IsAudioFile(args.Path))
|
||||
if (_libraryManager.IsAudioFile(args.Path))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
|
||||
@@ -105,7 +107,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
|
||||
|
||||
// For conflicting extensions, give priority to videos
|
||||
if (isMixedCollectionType && LibraryManager.IsVideoFile(args.Path))
|
||||
if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -180,7 +182,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||
var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
|
||||
|
||||
var resolver = new AudioBookListResolver(namingOptions);
|
||||
var resolverResult = resolver.Resolve(files).ToList();
|
||||
@@ -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;
|
||||
|
||||
@@ -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,8 +1,11 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscUtils.Udf;
|
||||
using Emby.Naming.Video;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -14,23 +17,23 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
/// <summary>
|
||||
/// Resolves a Path into a Video or Video subclass.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="T">The type of item to resolve.</typeparam>
|
||||
public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
|
||||
where T : Video, new()
|
||||
{
|
||||
protected readonly ILibraryManager LibraryManager;
|
||||
|
||||
protected BaseVideoResolver(ILibraryManager libraryManager)
|
||||
{
|
||||
LibraryManager = libraryManager;
|
||||
}
|
||||
|
||||
protected ILibraryManager LibraryManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the specified args.
|
||||
/// </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,14 +45,12 @@ 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();
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
|
||||
// If the path is a file check for a matching extensions
|
||||
var parser = new VideoResolver(namingOptions);
|
||||
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
TVideoType video = null;
|
||||
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
|
||||
{
|
||||
videoInfo = parser.ResolveDirectory(args.Path);
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
@@ -80,9 +81,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsBluRayDirectory(child.FullName, filename, args.DirectoryService))
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
videoInfo = parser.ResolveDirectory(args.Path);
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
@@ -100,7 +101,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
else if (IsDvdFile(filename))
|
||||
{
|
||||
videoInfo = parser.ResolveDirectory(args.Path);
|
||||
videoInfo = VideoResolver.ResolveDirectory(args.Path, namingOptions);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
@@ -130,7 +131,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
else
|
||||
{
|
||||
var videoInfo = parser.Resolve(args.Path, false, false);
|
||||
var videoInfo = VideoResolver.Resolve(args.Path, false, namingOptions, false);
|
||||
|
||||
if (videoInfo == null)
|
||||
{
|
||||
@@ -165,13 +166,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,14 +194,30 @@ 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;
|
||||
}
|
||||
else
|
||||
{
|
||||
// use disc-utils, both DVDs and BDs use UDF filesystem
|
||||
using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
UdfReader udfReader = new UdfReader(videoFileStream);
|
||||
if (udfReader.DirectoryExists("VIDEO_TS"))
|
||||
{
|
||||
video.IsoType = IsoType.Dvd;
|
||||
}
|
||||
else if (udfReader.DirectoryExists("BDMV"))
|
||||
{
|
||||
video.IsoType = IsoType.BluRay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,10 +267,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
|
||||
protected void Set3DFormat(Video video)
|
||||
{
|
||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||
|
||||
var resolver = new Format3DParser(namingOptions);
|
||||
var result = resolver.Parse(video.Path);
|
||||
var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
|
||||
|
||||
Set3DFormat(video, result.Is3D, result.Format3D);
|
||||
}
|
||||
@@ -282,25 +296,13 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is blu ray directory] [the specified directory name].
|
||||
/// Determines whether [is bluray directory] [the specified directory name].
|
||||
/// </summary>
|
||||
protected bool IsBluRayDirectory(string fullPath, string directoryName, IDirectoryService directoryService)
|
||||
/// <param name="directoryName">The directory name.</param>
|
||||
/// <returns>Whether the directory is a bluray directory.</returns>
|
||||
protected bool IsBluRayDirectory(string directoryName)
|
||||
{
|
||||
if (!string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
// var blurayExtensions = new[]
|
||||
//{
|
||||
// ".mts",
|
||||
// ".m2ts",
|
||||
// ".bdmv",
|
||||
// ".mpls"
|
||||
//};
|
||||
|
||||
// return directoryService.GetFiles(fullPath).Any(i => blurayExtensions.Contains(i.Extension ?? string.Empty, StringComparer.OrdinalIgnoreCase));
|
||||
return string.Equals(directoryName, "bdmv", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -13,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
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
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
@@ -7,10 +9,16 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
/// <summary>
|
||||
/// Class ItemResolver.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="T">The type of BaseItem.</typeparam>
|
||||
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,9 +1,12 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Emby.Naming.Video;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
@@ -69,159 +72,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return result;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
if (IsInvalid(parent, collectionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
// Owned items should just use the plain video type
|
||||
if (parent == null)
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (parent is Series || parent.GetParents().OfType<Series>().Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveVideos<T>(
|
||||
Folder parent,
|
||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||
IDirectoryService directoryService,
|
||||
bool suppportMultiEditions,
|
||||
string collectionType,
|
||||
bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var videos = new List<BaseItem>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||
|
||||
var resolver = new VideoListResolver(namingOptions);
|
||||
var resolverResult = resolver.Resolve(files, suppportMultiEditions).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = videos
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
|
||||
|
||||
foreach (var video in resolverResult)
|
||||
{
|
||||
var firstVideo = video.Files[0];
|
||||
|
||||
var videoItem = new T
|
||||
{
|
||||
Path = video.Files[0].Path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
ProductionYear = video.Year,
|
||||
Name = parseName ?
|
||||
video.Name :
|
||||
Path.GetFileNameWithoutExtension(video.Files[0].Path),
|
||||
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
|
||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
||||
SetVideoType(videoItem, firstVideo);
|
||||
Set3DFormat(videoItem, firstVideo);
|
||||
|
||||
result.Items.Add(videoItem);
|
||||
}
|
||||
|
||||
result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
// Ignore samples
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
result.Extras.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
|
||||
{
|
||||
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)
|
||||
public override Video Resolve(ItemResolveArgs args)
|
||||
{
|
||||
var collectionType = args.GetCollectionType();
|
||||
|
||||
@@ -320,6 +176,152 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return item;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveMultipleInternal(
|
||||
Folder parent,
|
||||
List<FileSystemMetadata> files,
|
||||
string collectionType,
|
||||
IDirectoryService directoryService)
|
||||
{
|
||||
if (IsInvalid(parent, collectionType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<MusicVideo>(parent, files, directoryService, true, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
// Owned items should just use the plain video type
|
||||
if (parent == null)
|
||||
{
|
||||
return ResolveVideos<Video>(parent, files, directoryService, false, collectionType, false);
|
||||
}
|
||||
|
||||
if (parent is Series || parent.GetParents().OfType<Series>().Any())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, false, collectionType, true);
|
||||
}
|
||||
|
||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ResolveVideos<Movie>(parent, files, directoryService, true, collectionType, true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private MultiItemResolverResult ResolveVideos<T>(
|
||||
Folder parent,
|
||||
IEnumerable<FileSystemMetadata> fileSystemEntries,
|
||||
IDirectoryService directoryService,
|
||||
bool suppportMultiEditions,
|
||||
string collectionType,
|
||||
bool parseName)
|
||||
where T : Video, new()
|
||||
{
|
||||
var files = new List<FileSystemMetadata>();
|
||||
var videos = new List<BaseItem>();
|
||||
var leftOver = new List<FileSystemMetadata>();
|
||||
|
||||
// Loop through each child file/folder and see if we find a video
|
||||
foreach (var child in fileSystemEntries)
|
||||
{
|
||||
// This is a hack but currently no better way to resolve a sometimes ambiguous situation
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
{
|
||||
if (string.Equals(child.Name, "tvshow.nfo", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(child.Name, "season.nfo", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (child.IsDirectory)
|
||||
{
|
||||
leftOver.Add(child);
|
||||
}
|
||||
else if (!IsIgnored(child.Name))
|
||||
{
|
||||
files.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
var namingOptions = LibraryManager.GetNamingOptions();
|
||||
|
||||
var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
|
||||
|
||||
var result = new MultiItemResolverResult
|
||||
{
|
||||
ExtraFiles = leftOver,
|
||||
Items = videos
|
||||
};
|
||||
|
||||
var isInMixedFolder = resolverResult.Count > 1 || (parent != null && parent.IsTopParent);
|
||||
|
||||
foreach (var video in resolverResult)
|
||||
{
|
||||
var firstVideo = video.Files[0];
|
||||
|
||||
var videoItem = new T
|
||||
{
|
||||
Path = video.Files[0].Path,
|
||||
IsInMixedFolder = isInMixedFolder,
|
||||
ProductionYear = video.Year,
|
||||
Name = parseName ?
|
||||
video.Name :
|
||||
Path.GetFileNameWithoutExtension(video.Files[0].Path),
|
||||
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
|
||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
||||
SetVideoType(videoItem, firstVideo);
|
||||
Set3DFormat(videoItem, firstVideo);
|
||||
|
||||
result.Items.Add(videoItem);
|
||||
}
|
||||
|
||||
result.ExtraFiles.AddRange(files.Where(i => !ContainsFile(resolverResult, i)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsIgnored(string filename)
|
||||
{
|
||||
// Ignore samples
|
||||
Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
|
||||
|
||||
return m.Success;
|
||||
}
|
||||
|
||||
private bool ContainsFile(List<VideoInfo> result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private bool ContainsFile(VideoInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return result.Files.Any(i => ContainsFile(i, file)) ||
|
||||
result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
|
||||
result.Extras.Any(i => ContainsFile(i, file));
|
||||
}
|
||||
|
||||
private static bool ContainsFile(VideoFileInfo result, FileSystemMetadata file)
|
||||
{
|
||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
|
||||
@@ -398,7 +400,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return movie;
|
||||
}
|
||||
|
||||
if (IsBluRayDirectory(child.FullName, filename, directoryService))
|
||||
if (IsBluRayDirectory(filename))
|
||||
{
|
||||
var movie = new T
|
||||
{
|
||||
@@ -479,7 +481,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return true;
|
||||
}
|
||||
|
||||
if (subfolders.Any(s => IsBluRayDirectory(s.FullName, s.Name, directoryService)))
|
||||
if (subfolders.Any(s => IsBluRayDirectory(s.Name)))
|
||||
{
|
||||
videoTypes.Add(VideoType.BluRay);
|
||||
return true;
|
||||
@@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
return returnVideo;
|
||||
}
|
||||
|
||||
private bool IsInvalid(Folder parent, string collectionType)
|
||||
private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
|
||||
{
|
||||
if (parent != null)
|
||||
{
|
||||
@@ -545,12 +547,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(collectionType))
|
||||
if (collectionType.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !_validCollectionTypes.Contains(collectionType, StringComparer.OrdinalIgnoreCase);
|
||||
return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -16,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
/// </summary>
|
||||
public class PlaylistResolver : FolderResolver<Playlist>
|
||||
{
|
||||
private string[] _musicPlaylistCollectionTypes = new string[] {
|
||||
private string[] _musicPlaylistCollectionTypes =
|
||||
{
|
||||
string.Empty,
|
||||
CollectionType.Music
|
||||
};
|
||||
@@ -63,7 +66,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,18 +1,19 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -13,8 +15,8 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Book = MediaBrowser.Controller.Entities.Book;
|
||||
using AudioBook = MediaBrowser.Controller.Entities.AudioBook;
|
||||
using Book = MediaBrowser.Controller.Entities.Book;
|
||||
|
||||
namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
@@ -175,6 +177,7 @@ namespace Emby.Server.Implementations.Library
|
||||
return dto;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UserItemDataDto GetUserDataDto(BaseItem item, BaseItemDto itemDto, User user, DtoOptions options)
|
||||
{
|
||||
var userData = GetUserData(user, item);
|
||||
@@ -189,7 +192,7 @@ namespace Emby.Server.Implementations.Library
|
||||
/// </summary>
|
||||
/// <param name="data">The data.</param>
|
||||
/// <returns>DtoUserItemData.</returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <c>null</c>.</exception>
|
||||
private UserItemDataDto GetUserItemDataDto(UserItemData data)
|
||||
{
|
||||
if (data == null)
|
||||
@@ -210,6 +213,7 @@ namespace Emby.Server.Implementations.Library
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdatePlayState(BaseItem item, UserItemData data, long? reportedPositionTicks)
|
||||
{
|
||||
var playedToCompletion = false;
|
||||
@@ -220,7 +224,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 && !(item is AudioBook))
|
||||
if (positionTicks > 0 && hasRuntime && item is not AudioBook && item is not Book)
|
||||
{
|
||||
var pctIn = decimal.Divide(positionTicks, runtimeTicks) * 100;
|
||||
|
||||
@@ -239,7 +243,7 @@ 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;
|
||||
@@ -248,15 +252,15 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
|
||||
{
|
||||
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
||||
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
||||
var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
||||
var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
||||
|
||||
if (minIn > _config.Configuration.MinAudiobookResume)
|
||||
if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
|
||||
{
|
||||
// ignore progress during the beginning
|
||||
positionTicks = 0;
|
||||
}
|
||||
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
||||
else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
||||
{
|
||||
// mark as completed close to the end
|
||||
positionTicks = 0;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -87,12 +87,15 @@ namespace Emby.Server.Implementations.Library.Validators
|
||||
|
||||
foreach (var item in deadEntities)
|
||||
{
|
||||
_logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
|
||||
_logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
|
||||
|
||||
_libraryManager.DeleteItem(item, new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
}, false);
|
||||
_libraryManager.DeleteItem(
|
||||
item,
|
||||
new DeleteOptions
|
||||
{
|
||||
DeleteFileLocation = false
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
progress.Report(100);
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return targetFile;
|
||||
}
|
||||
|
||||
public Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
public Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
if (directStreamProvider != null)
|
||||
{
|
||||
@@ -43,9 +43,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
private async Task RecordFromDirectStreamProvider(IDirectStreamProvider directStreamProvider, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(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();
|
||||
|
||||
@@ -68,9 +69,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Opened recording stream from tuner provider");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(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;
|
||||
@@ -158,8 +159,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
try
|
||||
{
|
||||
var recordingFolders = GetRecordingFolders().ToArray();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders()
|
||||
.ToList();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
var allExistingPaths = virtualFolders.SelectMany(i => i.Locations).ToList();
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
continue;
|
||||
}
|
||||
|
||||
var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo { Path = i }).ToArray();
|
||||
var mediaPathInfos = pathsToCreate.Select(i => new MediaPathInfo(i)).ToArray();
|
||||
|
||||
var libraryOptions = new LibraryOptions
|
||||
{
|
||||
@@ -209,7 +209,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
foreach (var path in pathsToRemove)
|
||||
{
|
||||
await RemovePathFromLibrary(path).ConfigureAwait(false);
|
||||
await RemovePathFromLibraryAsync(path).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -218,13 +218,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemovePathFromLibrary(string path)
|
||||
private async Task RemovePathFromLibraryAsync(string path)
|
||||
{
|
||||
_logger.LogDebug("Removing path from library: {0}", path);
|
||||
|
||||
var requiresRefresh = false;
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders()
|
||||
.ToList();
|
||||
var virtualFolders = _libraryManager.GetVirtualFolders();
|
||||
|
||||
foreach (var virtualFolder in virtualFolders)
|
||||
{
|
||||
@@ -459,7 +458,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
if (!string.IsNullOrWhiteSpace(tunerChannel.TunerChannelId))
|
||||
{
|
||||
var tunerChannelId = tunerChannel.TunerChannelId;
|
||||
if (tunerChannelId.IndexOf(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
if (tunerChannelId.Contains(".json.schedulesdirect.org", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tunerChannelId = tunerChannelId.Replace(".json.schedulesdirect.org", string.Empty, StringComparison.OrdinalIgnoreCase).TrimStart('I');
|
||||
}
|
||||
@@ -611,16 +610,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<string> CreateTimer(TimerInfo timer, CancellationToken cancellationToken)
|
||||
public Task<string> CreateTimer(TimerInfo info, CancellationToken cancellationToken)
|
||||
{
|
||||
var existingTimer = string.IsNullOrWhiteSpace(timer.ProgramId) ?
|
||||
var existingTimer = string.IsNullOrWhiteSpace(info.ProgramId) ?
|
||||
null :
|
||||
_timerProvider.GetTimerByProgramId(timer.ProgramId);
|
||||
_timerProvider.GetTimerByProgramId(info.ProgramId);
|
||||
|
||||
if (existingTimer != null)
|
||||
{
|
||||
if (existingTimer.Status == RecordingStatus.Cancelled ||
|
||||
existingTimer.Status == RecordingStatus.Completed)
|
||||
if (existingTimer.Status == RecordingStatus.Cancelled
|
||||
|| existingTimer.Status == RecordingStatus.Completed)
|
||||
{
|
||||
existingTimer.Status = RecordingStatus.New;
|
||||
existingTimer.IsManual = true;
|
||||
@@ -633,32 +632,32 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
timer.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
LiveTvProgram programInfo = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(timer.ProgramId))
|
||||
if (!string.IsNullOrWhiteSpace(info.ProgramId))
|
||||
{
|
||||
programInfo = GetProgramInfoFromCache(timer);
|
||||
programInfo = GetProgramInfoFromCache(info);
|
||||
}
|
||||
|
||||
if (programInfo == null)
|
||||
{
|
||||
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", timer.ProgramId);
|
||||
programInfo = GetProgramInfoFromCache(timer.ChannelId, timer.StartDate);
|
||||
_logger.LogInformation("Unable to find program with Id {0}. Will search using start date", info.ProgramId);
|
||||
programInfo = GetProgramInfoFromCache(info.ChannelId, info.StartDate);
|
||||
}
|
||||
|
||||
if (programInfo != null)
|
||||
{
|
||||
CopyProgramInfoToTimerInfo(programInfo, timer);
|
||||
CopyProgramInfoToTimerInfo(programInfo, info);
|
||||
}
|
||||
|
||||
timer.IsManual = true;
|
||||
_timerProvider.Add(timer);
|
||||
info.IsManual = true;
|
||||
_timerProvider.Add(info);
|
||||
|
||||
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(timer));
|
||||
TimerCreated?.Invoke(this, new GenericEventArgs<TimerInfo>(info));
|
||||
|
||||
return Task.FromResult(timer.Id);
|
||||
return Task.FromResult(info.Id);
|
||||
}
|
||||
|
||||
public async Task<string> CreateSeriesTimer(SeriesTimerInfo info, CancellationToken cancellationToken)
|
||||
@@ -802,22 +801,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -912,18 +911,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
var epgChannel = await GetEpgChannelFromTunerChannel(provider.Item1, provider.Item2, channel, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
List<ProgramInfo> programs;
|
||||
|
||||
if (epgChannel == null)
|
||||
{
|
||||
_logger.LogDebug("EPG channel not found for tuner channel {0}-{1} from {2}-{3}", channel.Number, channel.Name, provider.Item1.Name, provider.Item2.ListingsId ?? string.Empty);
|
||||
programs = new List<ProgramInfo>();
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
|
||||
|
||||
List<ProgramInfo> programs = (await provider.Item1.GetProgramsAsync(provider.Item2, epgChannel.Id, startDateUtc, endDateUtc, cancellationToken)
|
||||
.ConfigureAwait(false)).ToList();
|
||||
}
|
||||
|
||||
// Replace the value that came from the provider with a normalized value
|
||||
foreach (var program in programs)
|
||||
@@ -939,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
return new List<ProgramInfo>();
|
||||
return Enumerable.Empty<ProgramInfo>();
|
||||
}
|
||||
|
||||
private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders()
|
||||
@@ -1291,7 +1286,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Beginning recording. Will record for {0} minutes.", duration.TotalMinutes.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
_logger.LogInformation("Writing file to path: " + recordPath);
|
||||
_logger.LogInformation("Writing file to: {Path}", recordPath);
|
||||
|
||||
Action onStarted = async () =>
|
||||
{
|
||||
@@ -1416,13 +1411,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
private void TriggerRefresh(string path)
|
||||
{
|
||||
_logger.LogInformation("Triggering refresh on {path}", path);
|
||||
_logger.LogInformation("Triggering refresh on {Path}", path);
|
||||
|
||||
var item = GetAffectedBaseItem(Path.GetDirectoryName(path));
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
_logger.LogInformation("Refreshing recording parent {path}", item.Path);
|
||||
_logger.LogInformation("Refreshing recording parent {Path}", item.Path);
|
||||
|
||||
_providerManager.QueueRefresh(
|
||||
item.Id,
|
||||
@@ -1457,7 +1452,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
if (item.GetType() == typeof(Folder) && string.Equals(item.Path, parentPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var parentItem = item.GetParent();
|
||||
if (parentItem != null && !(parentItem is AggregateFolder))
|
||||
if (parentItem != null && parentItem is not AggregateFolder)
|
||||
{
|
||||
item = parentItem;
|
||||
}
|
||||
@@ -1511,8 +1506,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
DeleteLibraryItemsForTimers(timersToDelete);
|
||||
|
||||
var librarySeries = _libraryManager.FindByPath(seriesPath, true) as Folder;
|
||||
if (librarySeries == null)
|
||||
if (_libraryManager.FindByPath(seriesPath, true) is not Folder librarySeries)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1622,9 +1616,7 @@ 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)
|
||||
@@ -1668,7 +1660,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
_logger.LogInformation("Running recording post processor {0} {1}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
process.Exited += Process_Exited;
|
||||
process.Exited += OnProcessExited;
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -1682,7 +1674,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
return arguments.Replace("{path}", path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void Process_Exited(object sender, EventArgs e)
|
||||
private void OnProcessExited(object sender, EventArgs e)
|
||||
{
|
||||
using (var process = (Process)sender)
|
||||
{
|
||||
@@ -1856,7 +1848,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
|
||||
{
|
||||
@@ -1920,7 +1913,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
|
||||
{
|
||||
@@ -2238,14 +2232,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)
|
||||
{
|
||||
@@ -2604,7 +2594,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
Locations = new string[] { customPath },
|
||||
Name = "Recorded Movies",
|
||||
CollectionType = CollectionType.Movies
|
||||
CollectionType = CollectionTypeOptions.Movies
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2615,7 +2605,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;
|
||||
@@ -9,8 +11,9 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Library;
|
||||
@@ -28,7 +31,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
private readonly IServerApplicationPaths _appPaths;
|
||||
private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>();
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private bool _hasExited;
|
||||
private Stream _logFileStream;
|
||||
private string _targetPath;
|
||||
@@ -307,22 +310,15 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// TODO Investigate and properly fix.
|
||||
// Don't spam the log. This doesn't seem to throw in windows, but sometimes under linux
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reading ffmpeg recording log");
|
||||
|
||||
@@ -6,58 +6,45 @@ 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)
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
/// <summary>
|
||||
/// Records the specified media source.
|
||||
/// </summary>
|
||||
Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
|
||||
Task Record(IDirectStreamProvider? directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken);
|
||||
|
||||
string GetOutputPath(MediaSourceInfo mediaSource, string targetFile);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Json;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
private readonly string _dataPath;
|
||||
private readonly object _fileDataLock = new object();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private T[] _items;
|
||||
|
||||
public ItemDataProvider(
|
||||
@@ -47,11 +47,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
try
|
||||
{
|
||||
var jsonString = File.ReadAllText(_dataPath, Encoding.UTF8);
|
||||
_items = JsonSerializer.Deserialize<T[]>(jsonString, _jsonOptions);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
{
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>> TimerFired;
|
||||
public event EventHandler<GenericEventArgs<TimerInfo>>? TimerFired;
|
||||
|
||||
public void RestartTimers()
|
||||
{
|
||||
@@ -143,9 +143,9 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerCallback(object state)
|
||||
private void TimerCallback(object? state)
|
||||
{
|
||||
var timerId = (string)state;
|
||||
var timerId = (string?)state ?? throw new ArgumentNullException(nameof(state));
|
||||
|
||||
var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase));
|
||||
if (timer != null)
|
||||
@@ -154,12 +154,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
||||
}
|
||||
}
|
||||
|
||||
public TimerInfo GetTimer(string id)
|
||||
public TimerInfo? GetTimer(string id)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public TimerInfo GetTimerByProgramId(string programId)
|
||||
public TimerInfo? GetTimerByProgramId(string programId)
|
||||
{
|
||||
return GetAll().FirstOrDefault(r => string.Equals(r.ProgramId, programId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@@ -12,8 +14,9 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Json;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
@@ -35,8 +38,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
|
||||
private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private DateTime _lastErrorResponse;
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.GetOptions();
|
||||
|
||||
public SchedulesDirect(
|
||||
ILogger<SchedulesDirect> logger,
|
||||
@@ -94,12 +97,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
var dates = GetScheduleRequestDates(startDateUtc, endDateUtc);
|
||||
|
||||
_logger.LogInformation("Channel Station ID is: {ChannelID}", channelId);
|
||||
var requestList = new List<ScheduleDirect.RequestScheduleForChannel>()
|
||||
var requestList = new List<RequestScheduleForChannelDto>()
|
||||
{
|
||||
new ScheduleDirect.RequestScheduleForChannel()
|
||||
new RequestScheduleForChannelDto()
|
||||
{
|
||||
stationID = channelId,
|
||||
date = dates
|
||||
StationId = channelId,
|
||||
Date = dates
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,61 +114,61 @@ 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.DeserializeAsync<List<ScheduleDirect.Day>>(responseStream, _jsonOptions).ConfigureAwait(false);
|
||||
var dailySchedules = await JsonSerializer.DeserializeAsync<List<DayDto>>(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");
|
||||
programRequestOptions.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct();
|
||||
var programsID = dailySchedules.SelectMany(d => d.Programs.Select(s => s.ProgramId)).Distinct();
|
||||
programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json);
|
||||
|
||||
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.DeserializeAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream, _jsonOptions).ConfigureAwait(false);
|
||||
var programDict = programDetails.ToDictionary(p => p.programID, y => y);
|
||||
var programDetails = await JsonSerializer.DeserializeAsync<List<ProgramDetailsDto>>(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);
|
||||
|
||||
var programsInfo = new List<ProgramInfo>();
|
||||
foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs))
|
||||
foreach (ProgramDto schedule in dailySchedules.SelectMany(d => d.Programs))
|
||||
{
|
||||
// _logger.LogDebug("Proccesing Schedule for statio ID " + stationID +
|
||||
// " which corresponds to channel " + channelNumber + " and program id " +
|
||||
// schedule.programID + " which says it has images? " +
|
||||
// programDict[schedule.programID].hasImageArtwork);
|
||||
// schedule.ProgramId + " which says it has images? " +
|
||||
// programDict[schedule.ProgramId].hasImageArtwork);
|
||||
|
||||
if (images != null)
|
||||
{
|
||||
var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10));
|
||||
var imageIndex = images.FindIndex(i => i.ProgramId == schedule.ProgramId[..10]);
|
||||
if (imageIndex > -1)
|
||||
{
|
||||
var programEntry = programDict[schedule.programID];
|
||||
var programEntry = programDict[schedule.ProgramId];
|
||||
|
||||
var allImages = images[imageIndex].data ?? new List<ScheduleDirect.ImageData>();
|
||||
var imagesWithText = allImages.Where(i => string.Equals(i.text, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
var imagesWithoutText = allImages.Where(i => string.Equals(i.text, "no", StringComparison.OrdinalIgnoreCase));
|
||||
var allImages = images[imageIndex].Data ?? new List<ImageDataDto>();
|
||||
var imagesWithText = allImages.Where(i => string.Equals(i.Text, "yes", StringComparison.OrdinalIgnoreCase));
|
||||
var imagesWithoutText = allImages.Where(i => string.Equals(i.Text, "no", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
const double DesiredAspect = 2.0 / 3;
|
||||
|
||||
programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
|
||||
programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
|
||||
GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
|
||||
|
||||
const double WideAspect = 16.0 / 9;
|
||||
|
||||
programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
|
||||
programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
|
||||
|
||||
// Don't supply the same image twice
|
||||
if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal))
|
||||
if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
|
||||
{
|
||||
programEntry.thumbImage = null;
|
||||
programEntry.ThumbImage = null;
|
||||
}
|
||||
|
||||
programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
|
||||
programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
|
||||
|
||||
// programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
|
||||
// GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
|
||||
@@ -174,16 +177,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID]));
|
||||
programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.ProgramId]));
|
||||
}
|
||||
|
||||
return programsInfo;
|
||||
}
|
||||
|
||||
private static int GetSizeOrder(ScheduleDirect.ImageData image)
|
||||
private static int GetSizeOrder(ImageDataDto image)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(image.height)
|
||||
&& int.TryParse(image.height, out int value))
|
||||
if (int.TryParse(image.Height, out int value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
@@ -191,53 +193,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetChannelNumber(ScheduleDirect.Map map)
|
||||
private static string GetChannelNumber(MapDto map)
|
||||
{
|
||||
var channelNumber = map.logicalChannelNumber;
|
||||
var channelNumber = map.LogicalChannelNumber;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(channelNumber))
|
||||
{
|
||||
channelNumber = map.channel;
|
||||
channelNumber = map.Channel;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(channelNumber))
|
||||
{
|
||||
channelNumber = map.atscMajor + "." + map.atscMinor;
|
||||
channelNumber = map.AtscMajor + "." + map.AtscMinor;
|
||||
}
|
||||
|
||||
return channelNumber.TrimStart('0');
|
||||
}
|
||||
|
||||
private static bool IsMovie(ScheduleDirect.ProgramDetails programInfo)
|
||||
private static bool IsMovie(ProgramDetailsDto programInfo)
|
||||
{
|
||||
return string.Equals(programInfo.entityType, "movie", StringComparison.OrdinalIgnoreCase);
|
||||
return string.Equals(programInfo.EntityType, "movie", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private ProgramInfo GetProgram(string channelId, ScheduleDirect.Program programInfo, ScheduleDirect.ProgramDetails details)
|
||||
private ProgramInfo GetProgram(string channelId, ProgramDto programInfo, ProgramDetailsDto details)
|
||||
{
|
||||
var startAt = GetDate(programInfo.airDateTime);
|
||||
var endAt = startAt.AddSeconds(programInfo.duration);
|
||||
var startAt = GetDate(programInfo.AirDateTime);
|
||||
var endAt = startAt.AddSeconds(programInfo.Duration);
|
||||
var audioType = ProgramAudio.Stereo;
|
||||
|
||||
var programId = programInfo.programID ?? string.Empty;
|
||||
var programId = programInfo.ProgramId ?? string.Empty;
|
||||
|
||||
string newID = programId + "T" + startAt.Ticks + "C" + channelId;
|
||||
|
||||
if (programInfo.audioProperties != null)
|
||||
if (programInfo.AudioProperties != null)
|
||||
{
|
||||
if (programInfo.audioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
|
||||
if (programInfo.AudioProperties.Exists(item => string.Equals(item, "atmos", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
audioType = ProgramAudio.Atmos;
|
||||
}
|
||||
else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd 5.1", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
audioType = ProgramAudio.DolbyDigital;
|
||||
}
|
||||
else if (programInfo.audioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "dd", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
audioType = ProgramAudio.DolbyDigital;
|
||||
}
|
||||
else if (programInfo.audioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
|
||||
else if (programInfo.AudioProperties.Exists(item => string.Equals(item, "stereo", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
audioType = ProgramAudio.Stereo;
|
||||
}
|
||||
@@ -248,9 +250,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
|
||||
string episodeTitle = null;
|
||||
if (details.episodeTitle150 != null)
|
||||
if (details.EpisodeTitle150 != null)
|
||||
{
|
||||
episodeTitle = details.episodeTitle150;
|
||||
episodeTitle = details.EpisodeTitle150;
|
||||
}
|
||||
|
||||
var info = new ProgramInfo
|
||||
@@ -259,22 +261,22 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
Id = newID,
|
||||
StartDate = startAt,
|
||||
EndDate = endAt,
|
||||
Name = details.titles[0].title120 ?? "Unknown",
|
||||
Name = details.Titles[0].Title120 ?? "Unknown",
|
||||
OfficialRating = null,
|
||||
CommunityRating = null,
|
||||
EpisodeTitle = episodeTitle,
|
||||
Audio = audioType,
|
||||
// IsNew = programInfo.@new ?? false,
|
||||
IsRepeat = programInfo.@new == null,
|
||||
IsSeries = string.Equals(details.entityType, "episode", StringComparison.OrdinalIgnoreCase),
|
||||
ImageUrl = details.primaryImage,
|
||||
ThumbImageUrl = details.thumbImage,
|
||||
IsKids = string.Equals(details.audience, "children", StringComparison.OrdinalIgnoreCase),
|
||||
IsSports = string.Equals(details.entityType, "sports", StringComparison.OrdinalIgnoreCase),
|
||||
IsRepeat = programInfo.New == null,
|
||||
IsSeries = string.Equals(details.EntityType, "episode", StringComparison.OrdinalIgnoreCase),
|
||||
ImageUrl = details.PrimaryImage,
|
||||
ThumbImageUrl = details.ThumbImage,
|
||||
IsKids = string.Equals(details.Audience, "children", StringComparison.OrdinalIgnoreCase),
|
||||
IsSports = string.Equals(details.EntityType, "sports", StringComparison.OrdinalIgnoreCase),
|
||||
IsMovie = IsMovie(details),
|
||||
Etag = programInfo.md5,
|
||||
IsLive = string.Equals(programInfo.liveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
|
||||
IsPremiere = programInfo.premiere || (programInfo.isPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
|
||||
Etag = programInfo.Md5,
|
||||
IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase),
|
||||
IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1
|
||||
};
|
||||
|
||||
var showId = programId;
|
||||
@@ -297,15 +299,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
info.ShowId = showId;
|
||||
|
||||
if (programInfo.videoProperties != null)
|
||||
if (programInfo.VideoProperties != null)
|
||||
{
|
||||
info.IsHD = programInfo.videoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
|
||||
info.Is3D = programInfo.videoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
|
||||
info.IsHD = programInfo.VideoProperties.Contains("hdtv", StringComparer.OrdinalIgnoreCase);
|
||||
info.Is3D = programInfo.VideoProperties.Contains("3d", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (details.contentRating != null && details.contentRating.Count > 0)
|
||||
if (details.ContentRating != null && details.ContentRating.Count > 0)
|
||||
{
|
||||
info.OfficialRating = details.contentRating[0].code.Replace("TV", "TV-", StringComparison.Ordinal)
|
||||
info.OfficialRating = details.ContentRating[0].Code.Replace("TV", "TV-", StringComparison.Ordinal)
|
||||
.Replace("--", "-", StringComparison.Ordinal);
|
||||
|
||||
var invalid = new[] { "N/A", "Approved", "Not Rated", "Passed" };
|
||||
@@ -315,15 +317,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
if (details.descriptions != null)
|
||||
if (details.Descriptions != null)
|
||||
{
|
||||
if (details.descriptions.description1000 != null && details.descriptions.description1000.Count > 0)
|
||||
if (details.Descriptions.Description1000 != null && details.Descriptions.Description1000.Count > 0)
|
||||
{
|
||||
info.Overview = details.descriptions.description1000[0].description;
|
||||
info.Overview = details.Descriptions.Description1000[0].Description;
|
||||
}
|
||||
else if (details.descriptions.description100 != null && details.descriptions.description100.Count > 0)
|
||||
else if (details.Descriptions.Description100 != null && details.Descriptions.Description100.Count > 0)
|
||||
{
|
||||
info.Overview = details.descriptions.description100[0].description;
|
||||
info.Overview = details.Descriptions.Description100[0].Description;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,18 +335,18 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
|
||||
info.SeriesProviderIds[MetadataProvider.Zap2It.ToString()] = info.SeriesId;
|
||||
|
||||
if (details.metadata != null)
|
||||
if (details.Metadata != null)
|
||||
{
|
||||
foreach (var metadataProgram in details.metadata)
|
||||
foreach (var metadataProgram in details.Metadata)
|
||||
{
|
||||
var gracenote = metadataProgram.Gracenote;
|
||||
if (gracenote != null)
|
||||
{
|
||||
info.SeasonNumber = gracenote.season;
|
||||
info.SeasonNumber = gracenote.Season;
|
||||
|
||||
if (gracenote.episode > 0)
|
||||
if (gracenote.Episode > 0)
|
||||
{
|
||||
info.EpisodeNumber = gracenote.episode;
|
||||
info.EpisodeNumber = gracenote.Episode;
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -353,25 +355,25 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(details.originalAirDate))
|
||||
if (!string.IsNullOrWhiteSpace(details.OriginalAirDate))
|
||||
{
|
||||
info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture);
|
||||
info.OriginalAirDate = DateTime.Parse(details.OriginalAirDate, CultureInfo.InvariantCulture);
|
||||
info.ProductionYear = info.OriginalAirDate.Value.Year;
|
||||
}
|
||||
|
||||
if (details.movie != null)
|
||||
if (details.Movie != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(details.movie.year)
|
||||
&& int.TryParse(details.movie.year, out int year))
|
||||
if (!string.IsNullOrEmpty(details.Movie.Year)
|
||||
&& int.TryParse(details.Movie.Year, out int year))
|
||||
{
|
||||
info.ProductionYear = year;
|
||||
}
|
||||
}
|
||||
|
||||
if (details.genres != null)
|
||||
if (details.Genres != null)
|
||||
{
|
||||
info.Genres = details.genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
|
||||
info.IsNews = details.genres.Contains("news", StringComparer.OrdinalIgnoreCase);
|
||||
info.Genres = details.Genres.Where(g => !string.IsNullOrWhiteSpace(g)).ToList();
|
||||
info.IsNews = details.Genres.Contains("news", StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (info.Genres.Contains("children", StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -394,11 +396,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return date;
|
||||
}
|
||||
|
||||
private string GetProgramImage(string apiUrl, IEnumerable<ScheduleDirect.ImageData> images, bool returnDefaultImage, double desiredAspect)
|
||||
private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
|
||||
{
|
||||
var match = images
|
||||
.OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
|
||||
.ThenByDescending(GetSizeOrder)
|
||||
.ThenByDescending(i => GetSizeOrder(i))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (match == null)
|
||||
@@ -406,7 +408,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return null;
|
||||
}
|
||||
|
||||
var uri = match.uri;
|
||||
var uri = match.Uri;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(uri))
|
||||
{
|
||||
@@ -422,19 +424,19 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
}
|
||||
}
|
||||
|
||||
private static double GetAspectRatio(ScheduleDirect.ImageData i)
|
||||
private static double GetAspectRatio(ImageDataDto i)
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(i.width))
|
||||
if (!string.IsNullOrWhiteSpace(i.Width))
|
||||
{
|
||||
int.TryParse(i.width, out width);
|
||||
_ = int.TryParse(i.Width, out width);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(i.height))
|
||||
if (!string.IsNullOrWhiteSpace(i.Height))
|
||||
{
|
||||
int.TryParse(i.height, out height);
|
||||
_ = int.TryParse(i.Height, out height);
|
||||
}
|
||||
|
||||
if (height == 0 || width == 0)
|
||||
@@ -447,14 +449,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms(
|
||||
private async Task<List<ShowImagesDto>> GetImageForPrograms(
|
||||
ListingsProviderInfo info,
|
||||
IReadOnlyList<string> programIds,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (programIds.Count == 0)
|
||||
{
|
||||
return new List<ScheduleDirect.ShowImages>();
|
||||
return new List<ShowImagesDto>();
|
||||
}
|
||||
|
||||
StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
|
||||
@@ -478,13 +480,13 @@ 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.DeserializeAsync<List<ScheduleDirect.ShowImages>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<List<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting image info from schedules direct");
|
||||
|
||||
return new List<ScheduleDirect.ShowImages>();
|
||||
return new List<ShowImagesDto>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,18 +509,18 @@ 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.DeserializeAsync<List<ScheduleDirect.Headends>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<List<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (root != null)
|
||||
{
|
||||
foreach (ScheduleDirect.Headends headend in root)
|
||||
foreach (HeadendsDto headend in root)
|
||||
{
|
||||
foreach (ScheduleDirect.Lineup lineup in headend.lineups)
|
||||
foreach (LineupDto lineup in headend.Lineups)
|
||||
{
|
||||
lineups.Add(new NameIdPair
|
||||
{
|
||||
Name = string.IsNullOrWhiteSpace(lineup.name) ? lineup.lineup : lineup.name,
|
||||
Id = lineup.uri.Substring(18)
|
||||
Name = string.IsNullOrWhiteSpace(lineup.Name) ? lineup.Lineup : lineup.Name,
|
||||
Id = lineup.Uri[18..]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -648,14 +650,14 @@ 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.DeserializeAsync<ScheduleDirect.Token>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (string.Equals(root.message, "OK", StringComparison.Ordinal))
|
||||
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (string.Equals(root.Message, "OK", StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogInformation("Authenticated with Schedules Direct token: " + root.token);
|
||||
return root.token;
|
||||
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
|
||||
return root.Token;
|
||||
}
|
||||
|
||||
throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message);
|
||||
throw new Exception("Could not authenticate with Schedules Direct Error: " + root.Message);
|
||||
}
|
||||
|
||||
private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken)
|
||||
@@ -704,9 +706,9 @@ 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.DeserializeAsync<ScheduleDirect.Lineups>(stream, _jsonOptions).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return root.lineups.Any(i => string.Equals(info.ListingsId, i.lineup, StringComparison.OrdinalIgnoreCase));
|
||||
return root.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
@@ -776,38 +778,35 @@ 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.DeserializeAsync<ScheduleDirect.Channel>(stream, _jsonOptions).ConfigureAwait(false);
|
||||
_logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count);
|
||||
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(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 allStations = root.Stations ?? new List<StationDto>();
|
||||
|
||||
var map = root.map;
|
||||
var map = root.Map;
|
||||
var list = new List<ChannelInfo>(map.Count);
|
||||
foreach (var channel in map)
|
||||
{
|
||||
var channelNumber = GetChannelNumber(channel);
|
||||
|
||||
var station = allStations.Find(item => string.Equals(item.stationID, channel.stationID, StringComparison.OrdinalIgnoreCase));
|
||||
if (station == null)
|
||||
{
|
||||
station = new ScheduleDirect.Station
|
||||
var station = allStations.Find(item => string.Equals(item.StationId, channel.StationId, StringComparison.OrdinalIgnoreCase))
|
||||
?? new StationDto
|
||||
{
|
||||
stationID = channel.stationID
|
||||
StationId = channel.StationId
|
||||
};
|
||||
}
|
||||
|
||||
var channelInfo = new ChannelInfo
|
||||
{
|
||||
Id = station.stationID,
|
||||
CallSign = station.callsign,
|
||||
Id = station.StationId,
|
||||
CallSign = station.Callsign,
|
||||
Number = channelNumber,
|
||||
Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name
|
||||
Name = string.IsNullOrWhiteSpace(station.Name) ? channelNumber : station.Name
|
||||
};
|
||||
|
||||
if (station.logo != null)
|
||||
if (station.Logo != null)
|
||||
{
|
||||
channelInfo.ImageUrl = station.logo.URL;
|
||||
channelInfo.ImageUrl = station.Logo.Url;
|
||||
}
|
||||
|
||||
list.Add(channelInfo);
|
||||
@@ -820,402 +819,5 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
||||
{
|
||||
return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public class ScheduleDirect
|
||||
{
|
||||
public class Token
|
||||
{
|
||||
public int code { get; set; }
|
||||
|
||||
public string message { get; set; }
|
||||
|
||||
public string serverID { get; set; }
|
||||
|
||||
public string token { get; set; }
|
||||
}
|
||||
|
||||
public class Lineup
|
||||
{
|
||||
public string lineup { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string transport { get; set; }
|
||||
|
||||
public string location { get; set; }
|
||||
|
||||
public string uri { get; set; }
|
||||
}
|
||||
|
||||
public class Lineups
|
||||
{
|
||||
public int code { get; set; }
|
||||
|
||||
public string serverID { get; set; }
|
||||
|
||||
public string datetime { get; set; }
|
||||
|
||||
public List<Lineup> lineups { get; set; }
|
||||
}
|
||||
|
||||
public class Headends
|
||||
{
|
||||
public string headend { get; set; }
|
||||
|
||||
public string transport { get; set; }
|
||||
|
||||
public string location { get; set; }
|
||||
|
||||
public List<Lineup> lineups { get; set; }
|
||||
}
|
||||
|
||||
public class Map
|
||||
{
|
||||
public string stationID { get; set; }
|
||||
|
||||
public string channel { get; set; }
|
||||
|
||||
public string logicalChannelNumber { get; set; }
|
||||
|
||||
public int uhfVhf { get; set; }
|
||||
|
||||
public int atscMajor { get; set; }
|
||||
|
||||
public int atscMinor { get; set; }
|
||||
}
|
||||
|
||||
public class Broadcaster
|
||||
{
|
||||
public string city { get; set; }
|
||||
|
||||
public string state { get; set; }
|
||||
|
||||
public string postalcode { get; set; }
|
||||
|
||||
public string country { get; set; }
|
||||
}
|
||||
|
||||
public class Logo
|
||||
{
|
||||
public string URL { get; set; }
|
||||
|
||||
public int height { get; set; }
|
||||
|
||||
public int width { get; set; }
|
||||
|
||||
public string md5 { get; set; }
|
||||
}
|
||||
|
||||
public class Station
|
||||
{
|
||||
public string stationID { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string callsign { get; set; }
|
||||
|
||||
public List<string> broadcastLanguage { get; set; }
|
||||
|
||||
public List<string> descriptionLanguage { get; set; }
|
||||
|
||||
public Broadcaster broadcaster { get; set; }
|
||||
|
||||
public string affiliate { get; set; }
|
||||
|
||||
public Logo logo { get; set; }
|
||||
|
||||
public bool? isCommercialFree { get; set; }
|
||||
}
|
||||
|
||||
public class Metadata
|
||||
{
|
||||
public string lineup { get; set; }
|
||||
|
||||
public string modified { get; set; }
|
||||
|
||||
public string transport { get; set; }
|
||||
}
|
||||
|
||||
public class Channel
|
||||
{
|
||||
public List<Map> map { get; set; }
|
||||
|
||||
public List<Station> stations { get; set; }
|
||||
|
||||
public Metadata metadata { get; set; }
|
||||
}
|
||||
|
||||
public class RequestScheduleForChannel
|
||||
{
|
||||
public string stationID { get; set; }
|
||||
|
||||
public List<string> date { get; set; }
|
||||
}
|
||||
|
||||
public class Rating
|
||||
{
|
||||
public string body { get; set; }
|
||||
|
||||
public string code { get; set; }
|
||||
}
|
||||
|
||||
public class Multipart
|
||||
{
|
||||
public int partNumber { get; set; }
|
||||
|
||||
public int totalParts { get; set; }
|
||||
}
|
||||
|
||||
public class Program
|
||||
{
|
||||
public string programID { get; set; }
|
||||
|
||||
public string airDateTime { get; set; }
|
||||
|
||||
public int duration { get; set; }
|
||||
|
||||
public string md5 { get; set; }
|
||||
|
||||
public List<string> audioProperties { get; set; }
|
||||
|
||||
public List<string> videoProperties { get; set; }
|
||||
|
||||
public List<Rating> ratings { get; set; }
|
||||
|
||||
public bool? @new { get; set; }
|
||||
|
||||
public Multipart multipart { get; set; }
|
||||
|
||||
public string liveTapeDelay { get; set; }
|
||||
|
||||
public bool premiere { get; set; }
|
||||
|
||||
public bool repeat { get; set; }
|
||||
|
||||
public string isPremiereOrFinale { get; set; }
|
||||
}
|
||||
|
||||
public class MetadataSchedule
|
||||
{
|
||||
public string modified { get; set; }
|
||||
|
||||
public string md5 { get; set; }
|
||||
|
||||
public string startDate { get; set; }
|
||||
|
||||
public string endDate { get; set; }
|
||||
|
||||
public int days { get; set; }
|
||||
}
|
||||
|
||||
public class Day
|
||||
{
|
||||
public string stationID { get; set; }
|
||||
|
||||
public List<Program> programs { get; set; }
|
||||
|
||||
public MetadataSchedule metadata { get; set; }
|
||||
|
||||
public Day()
|
||||
{
|
||||
programs = new List<Program>();
|
||||
}
|
||||
}
|
||||
|
||||
public class Title
|
||||
{
|
||||
public string title120 { get; set; }
|
||||
}
|
||||
|
||||
public class EventDetails
|
||||
{
|
||||
public string subType { get; set; }
|
||||
}
|
||||
|
||||
public class Description100
|
||||
{
|
||||
public string descriptionLanguage { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
}
|
||||
|
||||
public class Description1000
|
||||
{
|
||||
public string descriptionLanguage { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
}
|
||||
|
||||
public class DescriptionsProgram
|
||||
{
|
||||
public List<Description100> description100 { get; set; }
|
||||
|
||||
public List<Description1000> description1000 { get; set; }
|
||||
}
|
||||
|
||||
public class Gracenote
|
||||
{
|
||||
public int season { get; set; }
|
||||
|
||||
public int episode { get; set; }
|
||||
}
|
||||
|
||||
public class MetadataPrograms
|
||||
{
|
||||
public Gracenote Gracenote { get; set; }
|
||||
}
|
||||
|
||||
public class ContentRating
|
||||
{
|
||||
public string body { get; set; }
|
||||
|
||||
public string code { get; set; }
|
||||
}
|
||||
|
||||
public class Cast
|
||||
{
|
||||
public string billingOrder { get; set; }
|
||||
|
||||
public string role { get; set; }
|
||||
|
||||
public string nameId { get; set; }
|
||||
|
||||
public string personId { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
|
||||
public string characterName { get; set; }
|
||||
}
|
||||
|
||||
public class Crew
|
||||
{
|
||||
public string billingOrder { get; set; }
|
||||
|
||||
public string role { get; set; }
|
||||
|
||||
public string nameId { get; set; }
|
||||
|
||||
public string personId { get; set; }
|
||||
|
||||
public string name { get; set; }
|
||||
}
|
||||
|
||||
public class QualityRating
|
||||
{
|
||||
public string ratingsBody { get; set; }
|
||||
|
||||
public string rating { get; set; }
|
||||
|
||||
public string minRating { get; set; }
|
||||
|
||||
public string maxRating { get; set; }
|
||||
|
||||
public string increment { get; set; }
|
||||
}
|
||||
|
||||
public class Movie
|
||||
{
|
||||
public string year { get; set; }
|
||||
|
||||
public int duration { get; set; }
|
||||
|
||||
public List<QualityRating> qualityRating { get; set; }
|
||||
}
|
||||
|
||||
public class Recommendation
|
||||
{
|
||||
public string programID { get; set; }
|
||||
|
||||
public string title120 { get; set; }
|
||||
}
|
||||
|
||||
public class ProgramDetails
|
||||
{
|
||||
public string audience { get; set; }
|
||||
|
||||
public string programID { get; set; }
|
||||
|
||||
public List<Title> titles { get; set; }
|
||||
|
||||
public EventDetails eventDetails { get; set; }
|
||||
|
||||
public DescriptionsProgram descriptions { get; set; }
|
||||
|
||||
public string originalAirDate { get; set; }
|
||||
|
||||
public List<string> genres { get; set; }
|
||||
|
||||
public string episodeTitle150 { get; set; }
|
||||
|
||||
public List<MetadataPrograms> metadata { get; set; }
|
||||
|
||||
public List<ContentRating> contentRating { get; set; }
|
||||
|
||||
public List<Cast> cast { get; set; }
|
||||
|
||||
public List<Crew> crew { get; set; }
|
||||
|
||||
public string entityType { get; set; }
|
||||
|
||||
public string showType { get; set; }
|
||||
|
||||
public bool hasImageArtwork { get; set; }
|
||||
|
||||
public string primaryImage { get; set; }
|
||||
|
||||
public string thumbImage { get; set; }
|
||||
|
||||
public string backdropImage { get; set; }
|
||||
|
||||
public string bannerImage { get; set; }
|
||||
|
||||
public string imageID { get; set; }
|
||||
|
||||
public string md5 { get; set; }
|
||||
|
||||
public List<string> contentAdvisory { get; set; }
|
||||
|
||||
public Movie movie { get; set; }
|
||||
|
||||
public List<Recommendation> recommendations { get; set; }
|
||||
}
|
||||
|
||||
public class Caption
|
||||
{
|
||||
public string content { get; set; }
|
||||
|
||||
public string lang { get; set; }
|
||||
}
|
||||
|
||||
public class ImageData
|
||||
{
|
||||
public string width { get; set; }
|
||||
|
||||
public string height { get; set; }
|
||||
|
||||
public string uri { get; set; }
|
||||
|
||||
public string size { get; set; }
|
||||
|
||||
public string aspect { get; set; }
|
||||
|
||||
public string category { get; set; }
|
||||
|
||||
public string text { get; set; }
|
||||
|
||||
public string primary { get; set; }
|
||||
|
||||
public string tier { get; set; }
|
||||
|
||||
public Caption caption { get; set; }
|
||||
}
|
||||
|
||||
public class ShowImages
|
||||
{
|
||||
public string programID { get; set; }
|
||||
|
||||
public List<ImageData> data { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcaster dto.
|
||||
/// </summary>
|
||||
public class BroadcasterDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
[JsonPropertyName("city")]
|
||||
public string City { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the postal code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("postalCode")]
|
||||
public string Postalcode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
[JsonPropertyName("country")]
|
||||
public string Country { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Caption dto.
|
||||
/// </summary>
|
||||
public class CaptionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the content.
|
||||
/// </summary>
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lang.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lang")]
|
||||
public string Lang { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Cast dto.
|
||||
/// </summary>
|
||||
public class CastDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the character name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("characterName")]
|
||||
public string CharacterName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Channel dto.
|
||||
/// </summary>
|
||||
public class ChannelDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of maps.
|
||||
/// </summary>
|
||||
[JsonPropertyName("map")]
|
||||
public List<MapDto> Map { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of stations.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stations")]
|
||||
public List<StationDto> Stations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataDto Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Content rating dto.
|
||||
/// </summary>
|
||||
public class ContentRatingDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the body.
|
||||
/// </summary>
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Crew dto.
|
||||
/// </summary>
|
||||
public class CrewDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the billing order.
|
||||
/// </summary>
|
||||
[JsonPropertyName("billingOrder")]
|
||||
public string BillingOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("nameId")]
|
||||
public string NameId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the person id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("personId")]
|
||||
public string PersonId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Day dto.
|
||||
/// </summary>
|
||||
public class DayDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DayDto"/> class.
|
||||
/// </summary>
|
||||
public DayDto()
|
||||
{
|
||||
Programs = new List<ProgramDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the station id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("stationID")]
|
||||
public string StationId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of programs.
|
||||
/// </summary>
|
||||
[JsonPropertyName("programs")]
|
||||
public List<ProgramDto> Programs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the metadata schedule.
|
||||
/// </summary>
|
||||
[JsonPropertyName("metadata")]
|
||||
public MetadataScheduleDto Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Description 1_000 dto.
|
||||
/// </summary>
|
||||
public class Description1000Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Description 100 dto.
|
||||
/// </summary>
|
||||
public class Description100Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the description language.
|
||||
/// </summary>
|
||||
[JsonPropertyName("descriptionLanguage")]
|
||||
public string DescriptionLanguage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Descriptions program dto.
|
||||
/// </summary>
|
||||
public class DescriptionsProgramDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of description 100.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description100")]
|
||||
public List<Description100Dto> Description100 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of description1000.
|
||||
/// </summary>
|
||||
[JsonPropertyName("description1000")]
|
||||
public List<Description1000Dto> Description1000 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Event details dto.
|
||||
/// </summary>
|
||||
public class EventDetailsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the sub type.
|
||||
/// </summary>
|
||||
[JsonPropertyName("subType")]
|
||||
public string SubType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Gracenote dto.
|
||||
/// </summary>
|
||||
public class GracenoteDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the season.
|
||||
/// </summary>
|
||||
[JsonPropertyName("season")]
|
||||
public int Season { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the episode.
|
||||
/// </summary>
|
||||
[JsonPropertyName("episode")]
|
||||
public int Episode { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Headends dto.
|
||||
/// </summary>
|
||||
public class HeadendsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the headend.
|
||||
/// </summary>
|
||||
[JsonPropertyName("headend")]
|
||||
public string Headend { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public List<LineupDto> Lineups { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Image data dto.
|
||||
/// </summary>
|
||||
public class ImageDataDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
[JsonPropertyName("width")]
|
||||
public string Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
[JsonPropertyName("height")]
|
||||
public string Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the size.
|
||||
/// </summary>
|
||||
[JsonPropertyName("size")]
|
||||
public string Size { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aspect.
|
||||
/// </summary>
|
||||
[JsonPropertyName("aspect")]
|
||||
public string aspect { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the category.
|
||||
/// </summary>
|
||||
[JsonPropertyName("category")]
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text.
|
||||
/// </summary>
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the primary.
|
||||
/// </summary>
|
||||
[JsonPropertyName("primary")]
|
||||
public string Primary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("tier")]
|
||||
public string Tier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the caption.
|
||||
/// </summary>
|
||||
[JsonPropertyName("caption")]
|
||||
public CaptionDto Caption { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// The lineup dto.
|
||||
/// </summary>
|
||||
public class LineupDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the linup.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineup")]
|
||||
public string Lineup { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the lineup name.
|
||||
/// </summary>
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the transport.
|
||||
/// </summary>
|
||||
[JsonPropertyName("transport")]
|
||||
public string Transport { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the location.
|
||||
/// </summary>
|
||||
[JsonPropertyName("location")]
|
||||
public string Location { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the uri.
|
||||
/// </summary>
|
||||
[JsonPropertyName("uri")]
|
||||
public string Uri { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos
|
||||
{
|
||||
/// <summary>
|
||||
/// Lineups dto.
|
||||
/// </summary>
|
||||
public class LineupsDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the response code.
|
||||
/// </summary>
|
||||
[JsonPropertyName("code")]
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverID")]
|
||||
public string ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the datetime.
|
||||
/// </summary>
|
||||
[JsonPropertyName("datetime")]
|
||||
public string Datetime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lineups.
|
||||
/// </summary>
|
||||
[JsonPropertyName("lineups")]
|
||||
public List<LineupDto> Lineups { get; set; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user