diff options
| author | nyanmisaka <nst799610810@gmail.com> | 2020-11-08 19:11:54 +0800 |
|---|---|---|
| committer | nyanmisaka <nst799610810@gmail.com> | 2020-11-08 19:11:54 +0800 |
| commit | 737cb727f9f943f8bb55fca8a5c5023a98aca2d7 (patch) | |
| tree | 8858a423545a03d0f7dfe5c1d1b0ae1157d62c5e /Emby.Server.Implementations | |
| parent | 05e78ee78c56364971956507f6239ded61f0af87 (diff) | |
| parent | 96dcd9c87e2eb4b14004368856949e9fde2db261 (diff) | |
Merge remote-tracking branch 'upstream/master' into fonts
Diffstat (limited to 'Emby.Server.Implementations')
167 files changed, 2757 insertions, 7477 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs deleted file mode 100644 index 84bec9201..000000000 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ /dev/null @@ -1,590 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Controller.Subtitles; -using MediaBrowser.Model.Activity; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Notifications; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Activity -{ - /// <summary> - /// Entry point for the activity logger. - /// </summary> - public sealed class ActivityLogEntryPoint : IServerEntryPoint - { - private readonly ILogger<ActivityLogEntryPoint> _logger; - private readonly IInstallationManager _installationManager; - private readonly ISessionManager _sessionManager; - private readonly ITaskManager _taskManager; - private readonly IActivityManager _activityManager; - private readonly ILocalizationManager _localization; - private readonly ISubtitleManager _subManager; - private readonly IUserManager _userManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="sessionManager">The session manager.</param> - /// <param name="taskManager">The task manager.</param> - /// <param name="activityManager">The activity manager.</param> - /// <param name="localization">The localization manager.</param> - /// <param name="installationManager">The installation manager.</param> - /// <param name="subManager">The subtitle manager.</param> - /// <param name="userManager">The user manager.</param> - public ActivityLogEntryPoint( - ILogger<ActivityLogEntryPoint> logger, - ISessionManager sessionManager, - ITaskManager taskManager, - IActivityManager activityManager, - ILocalizationManager localization, - IInstallationManager installationManager, - ISubtitleManager subManager, - IUserManager userManager) - { - _logger = logger; - _sessionManager = sessionManager; - _taskManager = taskManager; - _activityManager = activityManager; - _localization = localization; - _installationManager = installationManager; - _subManager = subManager; - _userManager = userManager; - } - - /// <inheritdoc /> - public Task RunAsync() - { - _taskManager.TaskCompleted += OnTaskCompleted; - - _installationManager.PluginInstalled += OnPluginInstalled; - _installationManager.PluginUninstalled += OnPluginUninstalled; - _installationManager.PluginUpdated += OnPluginUpdated; - _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - - _sessionManager.SessionStarted += OnSessionStarted; - _sessionManager.AuthenticationFailed += OnAuthenticationFailed; - _sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded; - _sessionManager.SessionEnded += OnSessionEnded; - _sessionManager.PlaybackStart += OnPlaybackStart; - _sessionManager.PlaybackStopped += OnPlaybackStopped; - - _subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure; - - _userManager.OnUserCreated += OnUserCreated; - _userManager.OnUserPasswordChanged += OnUserPasswordChanged; - _userManager.OnUserDeleted += OnUserDeleted; - _userManager.OnUserLockedOut += OnUserLockedOut; - - return Task.CompletedTask; - } - - private async void OnUserLockedOut(object sender, GenericEventArgs<User> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserLockedOutWithName"), - e.Argument.Username), - NotificationType.UserLockedOut.ToString(), - e.Argument.Id) - { - LogSeverity = LogLevel.Error - }).ConfigureAwait(false); - } - - private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"), - e.Provider, - Notifications.NotificationEntryPoint.GetItemName(e.Item)), - "SubtitleDownloadFailure", - Guid.Empty) - { - ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture), - ShortOverview = e.Exception.Message - }).ConfigureAwait(false); - } - - private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.LogWarning("PlaybackStopped reported with null media info."); - return; - } - - if (e.Item != null && e.Item.IsThemeMedia) - { - // Don't report theme song or local trailer playback - return; - } - - if (e.Users.Count == 0) - { - return; - } - - var user = e.Users[0]; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), - user.Username, - GetItemName(item), - e.DeviceName), - GetPlaybackStoppedNotificationType(item.MediaType), - user.Id)) - .ConfigureAwait(false); - } - - private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e) - { - var item = e.MediaInfo; - - if (item == null) - { - _logger.LogWarning("PlaybackStart reported with null media info."); - return; - } - - if (e.Item != null && e.Item.IsThemeMedia) - { - // Don't report theme song or local trailer playback - return; - } - - if (e.Users.Count == 0) - { - return; - } - - var user = e.Users.First(); - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserStartedPlayingItemWithValues"), - user.Username, - GetItemName(item), - e.DeviceName), - GetPlaybackNotificationType(item.MediaType), - user.Id)) - .ConfigureAwait(false); - } - - private static string GetItemName(BaseItemDto item) - { - var name = item.Name; - - if (!string.IsNullOrEmpty(item.SeriesName)) - { - name = item.SeriesName + " - " + name; - } - - if (item.Artists != null && item.Artists.Count > 0) - { - name = item.Artists[0] + " - " + name; - } - - return name; - } - - private static string GetPlaybackNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlayback.ToString(); - } - - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlayback.ToString(); - } - - return null; - } - - private static string GetPlaybackStoppedNotificationType(string mediaType) - { - if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.AudioPlaybackStopped.ToString(); - } - - if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) - { - return NotificationType.VideoPlaybackStopped.ToString(); - } - - return null; - } - - private async void OnSessionEnded(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - - if (string.IsNullOrEmpty(session.UserName)) - { - return; - } - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserOfflineFromDevice"), - session.UserName, - session.DeviceName), - "SessionEnded", - session.UserId) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e) - { - var user = e.Argument.User; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("AuthenticationSucceededWithUserName"), - user.Name), - "AuthenticationSucceeded", - user.Id) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - e.Argument.SessionInfo.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("FailedLoginAttemptWithUserName"), - e.Argument.Username), - "AuthenticationFailed", - Guid.Empty) - { - LogSeverity = LogLevel.Error, - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - e.Argument.RemoteEndPoint), - }).ConfigureAwait(false); - } - - private async void OnUserDeleted(object sender, GenericEventArgs<User> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserDeletedWithName"), - e.Argument.Username), - "UserDeleted", - Guid.Empty)) - .ConfigureAwait(false); - } - - private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserPasswordChangedWithName"), - e.Argument.Username), - "UserPasswordChanged", - e.Argument.Id)) - .ConfigureAwait(false); - } - - private async void OnUserCreated(object sender, GenericEventArgs<User> e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserCreatedWithName"), - e.Argument.Username), - "UserCreated", - e.Argument.Id)) - .ConfigureAwait(false); - } - - private async void OnSessionStarted(object sender, SessionEventArgs e) - { - var session = e.SessionInfo; - - if (string.IsNullOrEmpty(session.UserName)) - { - return; - } - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("UserOnlineFromDevice"), - session.UserName, - session.DeviceName), - "SessionStarted", - session.UserId) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelIpAddressValue"), - session.RemoteEndPoint) - }).ConfigureAwait(false); - } - - private async void OnPluginUpdated(object sender, InstallationInfo e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginUpdatedWithName"), - e.Name), - NotificationType.PluginUpdateInstalled.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - e.Version), - Overview = e.Changelog - }).ConfigureAwait(false); - } - - private async void OnPluginUninstalled(object sender, IPlugin e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginUninstalledWithName"), - e.Name), - NotificationType.PluginUninstalled.ToString(), - Guid.Empty)) - .ConfigureAwait(false); - } - - private async void OnPluginInstalled(object sender, InstallationInfo e) - { - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("PluginInstalledWithName"), - e.Name), - NotificationType.PluginInstalled.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - e.Version) - }).ConfigureAwait(false); - } - - private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) - { - var installationInfo = e.InstallationInfo; - - await CreateLogEntry(new ActivityLog( - string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("NameInstallFailed"), - installationInfo.Name), - NotificationType.InstallationFailed.ToString(), - Guid.Empty) - { - ShortOverview = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("VersionNumber"), - installationInfo.Version), - Overview = e.Exception.Message - }).ConfigureAwait(false); - } - - private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) - { - var result = e.Result; - var task = e.Task; - - if (task.ScheduledTask is IConfigurableScheduledTask activityTask - && !activityTask.IsLogged) - { - return; - } - - var time = result.EndTimeUtc - result.StartTimeUtc; - var runningTime = string.Format( - CultureInfo.InvariantCulture, - _localization.GetLocalizedString("LabelRunningTimeValue"), - ToUserFriendlyString(time)); - - if (result.Status == TaskCompletionStatus.Failed) - { - var vals = new List<string>(); - - if (!string.IsNullOrEmpty(e.Result.ErrorMessage)) - { - vals.Add(e.Result.ErrorMessage); - } - - if (!string.IsNullOrEmpty(e.Result.LongErrorMessage)) - { - vals.Add(e.Result.LongErrorMessage); - } - - await CreateLogEntry(new ActivityLog( - string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name), - NotificationType.TaskFailed.ToString(), - Guid.Empty) - { - LogSeverity = LogLevel.Error, - Overview = string.Join(Environment.NewLine, vals), - ShortOverview = runningTime - }).ConfigureAwait(false); - } - } - - private async Task CreateLogEntry(ActivityLog entry) - => await _activityManager.CreateAsync(entry).ConfigureAwait(false); - - /// <inheritdoc /> - public void Dispose() - { - _taskManager.TaskCompleted -= OnTaskCompleted; - - _installationManager.PluginInstalled -= OnPluginInstalled; - _installationManager.PluginUninstalled -= OnPluginUninstalled; - _installationManager.PluginUpdated -= OnPluginUpdated; - _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - - _sessionManager.SessionStarted -= OnSessionStarted; - _sessionManager.AuthenticationFailed -= OnAuthenticationFailed; - _sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded; - _sessionManager.SessionEnded -= OnSessionEnded; - - _sessionManager.PlaybackStart -= OnPlaybackStart; - _sessionManager.PlaybackStopped -= OnPlaybackStopped; - - _subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure; - - _userManager.OnUserCreated -= OnUserCreated; - _userManager.OnUserPasswordChanged -= OnUserPasswordChanged; - _userManager.OnUserDeleted -= OnUserDeleted; - _userManager.OnUserLockedOut -= OnUserLockedOut; - } - - /// <summary> - /// Constructs a user-friendly string for this TimeSpan instance. - /// </summary> - private static string ToUserFriendlyString(TimeSpan span) - { - const int DaysInYear = 365; - const int DaysInMonth = 30; - - // Get each non-zero value from TimeSpan component - var values = new List<string>(); - - // Number of years - int days = span.Days; - if (days >= DaysInYear) - { - int years = days / DaysInYear; - values.Add(CreateValueString(years, "year")); - days %= DaysInYear; - } - - // Number of months - if (days >= DaysInMonth) - { - int months = days / DaysInMonth; - values.Add(CreateValueString(months, "month")); - days = days % DaysInMonth; - } - - // Number of days - if (days >= 1) - { - values.Add(CreateValueString(days, "day")); - } - - // Number of hours - if (span.Hours >= 1) - { - values.Add(CreateValueString(span.Hours, "hour")); - } - - // Number of minutes - if (span.Minutes >= 1) - { - values.Add(CreateValueString(span.Minutes, "minute")); - } - - // Number of seconds (include when 0 if no other components included) - if (span.Seconds >= 1 || values.Count == 0) - { - values.Add(CreateValueString(span.Seconds, "second")); - } - - // Combine values into string - var builder = new StringBuilder(); - for (int i = 0; i < values.Count; i++) - { - if (builder.Length > 0) - { - builder.Append(i == values.Count - 1 ? " and " : ", "); - } - - builder.Append(values[i]); - } - - // Return result - return builder.ToString(); - } - - /// <summary> - /// Constructs a string description of a time-span value. - /// </summary> - /// <param name="value">The value of this item.</param> - /// <param name="description">The name of this item (singular form).</param> - private static string CreateValueString(int value, string description) - { - return string.Format( - CultureInfo.InvariantCulture, - "{0:#,##0} {1}", - value, - value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description)); - } - } -} diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 2adc1d6c3..660bbb2de 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.AppBase } /// <inheritdoc /> - public string VirtualDataPath { get; } = "%AppDataPath%"; + public string VirtualDataPath => "%AppDataPath%"; /// <summary> /// Gets the image cache path. diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index d4a8268b9..4ab0a2a3f 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception ex) { - Logger.LogError(ex, "Error loading configuration file: {path}", path); + Logger.LogError(ex, "Error loading configuration file: {Path}", path); return Activator.CreateInstance(configurationType); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 0201ed7a3..58edbe987 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Net; @@ -37,10 +38,11 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Net; 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.Services; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; @@ -49,11 +51,11 @@ using Jellyfin.Api.Helpers; 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; using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Collections; @@ -72,6 +74,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -89,19 +92,20 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.Chapters; using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; +using MediaBrowser.Providers.Plugins.Tmdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; 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 { @@ -118,18 +122,22 @@ namespace Emby.Server.Implementations private readonly IFileSystem _fileSystemManager; private readonly INetworkManager _networkManager; private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; private readonly IStartupOptions _startupOptions; private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private IHttpServer _httpServer; - private IHttpClient _httpClient; + private IHttpClientFactory _httpClientFactory; + + private string[] _urlPrefixes; /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> public bool CanSelfRestart => _startupOptions.RestartPath != null; + public bool CoreStartupHasCompleted { get; private set; } + public virtual bool CanLaunchWebBrowser { get @@ -173,6 +181,8 @@ namespace Emby.Server.Implementations /// </summary> protected ILogger<ApplicationHost> Logger { get; } + protected IServiceCollection ServiceCollection { get; } + private IPlugin[] _plugins; /// <summary> @@ -231,16 +241,26 @@ namespace Emby.Server.Implementations public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager; /// <summary> - /// Initializes a new instance of the <see cref="ApplicationHost" /> class. + /// 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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> + /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param> public ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, IFileSystem fileSystem, - INetworkManager networkManager) + INetworkManager networkManager, + IServiceCollection serviceCollection) { _xmlSerializer = new MyXmlSerializer(); + _jsonSerializer = new JsonSerializer(); + + ServiceCollection = serviceCollection; _networkManager = networkManager; networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets; @@ -271,6 +291,10 @@ namespace Emby.Server.Implementations Password = ServerConfigurationManager.Configuration.CertificatePassword }; Certificate = GetCertificate(CertificateInfo); + + ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; + ApplicationVersionString = ApplicationVersion.ToString(3); + ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } public string ExpandVirtualPath(string path) @@ -300,22 +324,22 @@ namespace Emby.Server.Implementations } /// <inheritdoc /> - public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version; + public Version ApplicationVersion { get; } /// <inheritdoc /> - public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); + public string ApplicationVersionString { get; } /// <summary> /// Gets the current application user agent. /// </summary> /// <value>The application user agent.</value> - public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString; + public string ApplicationUserAgent { get; } /// <summary> /// Gets the email address for use within a comment section of a user agent field. /// Presently used to provide contact information to MusicBrainz service. /// </summary> - public string ApplicationUserAgentAddress { get; } = "team@jellyfin.org"; + public string ApplicationUserAgentAddress => "team@jellyfin.org"; /// <summary> /// Gets the current application name. @@ -379,7 +403,7 @@ namespace Emby.Server.Implementations /// <summary> /// Resolves this instance. /// </summary> - /// <typeparam name="T">The type</typeparam> + /// <typeparam name="T">The type.</typeparam> /// <returns>``0.</returns> public T Resolve<T>() => ServiceProvider.GetService<T>(); @@ -440,8 +464,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - _httpServer.GlobalResponse = null; - + CoreStartupHasCompleted = true; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); @@ -464,7 +487,7 @@ namespace Emby.Server.Implementations } /// <inheritdoc/> - public void Init(IServiceCollection serviceCollection) + public void Init() { HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; @@ -493,145 +516,142 @@ namespace Emby.Server.Implementations DiscoverTypes(); - RegisterServices(serviceCollection); + RegisterServices(); } - public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) - => _httpServer.RequestHandler(context); - /// <summary> /// Registers services/resources with the service collection that will be available via DI. /// </summary> - protected virtual void RegisterServices(IServiceCollection serviceCollection) + protected virtual void RegisterServices() { - serviceCollection.AddSingleton(_startupOptions); - - serviceCollection.AddMemoryCache(); - - serviceCollection.AddSingleton(ConfigurationManager); - serviceCollection.AddSingleton<IApplicationHost>(this); + ServiceCollection.AddSingleton(_startupOptions); - serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); + ServiceCollection.AddMemoryCache(); - serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); + ServiceCollection.AddSingleton(ConfigurationManager); + ServiceCollection.AddSingleton<IApplicationHost>(this); - serviceCollection.AddSingleton(_fileSystemManager); - serviceCollection.AddSingleton<TvdbClientManager>(); + ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>(); + ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>(); - serviceCollection.AddSingleton(_networkManager); + ServiceCollection.AddSingleton(_fileSystemManager); + ServiceCollection.AddSingleton<TvdbClientManager>(); + ServiceCollection.AddSingleton<TmdbClientManager>(); - serviceCollection.AddSingleton<IIsoManager, IsoManager>(); + ServiceCollection.AddSingleton(_networkManager); - serviceCollection.AddSingleton<ITaskManager, TaskManager>(); + ServiceCollection.AddSingleton<IIsoManager, IsoManager>(); - serviceCollection.AddSingleton(_xmlSerializer); + ServiceCollection.AddSingleton<ITaskManager, TaskManager>(); - serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); + ServiceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); + ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>(); - serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); + ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); - serviceCollection.AddSingleton<IInstallationManager, InstallationManager>(); + ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>(); - serviceCollection.AddSingleton<IZipClient, ZipClient>(); + ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>(); - serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>(); + ServiceCollection.AddSingleton<IZipClient, ZipClient>(); - serviceCollection.AddSingleton<IServerApplicationHost>(this); - serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); + ServiceCollection.AddSingleton<IServerApplicationHost>(this); + ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); - serviceCollection.AddSingleton(ServerConfigurationManager); + ServiceCollection.AddSingleton(ServerConfigurationManager); - serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); + ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>(); - serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); + ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); - serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); - serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); + ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); + ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>(); - serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); + ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>(); - serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>(); + 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>)); + 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.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>)); + ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required - serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); - serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); - serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); - serviceCollection.AddSingleton<ILibraryManager, LibraryManager>(); + ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>)); + ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>)); + ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>)); + ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>(); - serviceCollection.AddSingleton<IMusicManager, MusicManager>(); + ServiceCollection.AddSingleton<IMusicManager, MusicManager>(); - serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); + ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>(); - serviceCollection.AddSingleton<ISearchEngine, SearchEngine>(); + ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); - serviceCollection.AddSingleton<ServiceController>(); - serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); + ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); - serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); + ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); - serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); + ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>(); - serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); + ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>(); - serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); + ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>(); - serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); + ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>(); - serviceCollection.AddSingleton<IProviderManager, ProviderManager>(); + ServiceCollection.AddSingleton<IProviderManager, ProviderManager>(); // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required - serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); - serviceCollection.AddSingleton<IDtoService, DtoService>(); + ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); + ServiceCollection.AddSingleton<IDtoService, DtoService>(); - serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); + ServiceCollection.AddSingleton<IChannelManager, ChannelManager>(); - serviceCollection.AddSingleton<ISessionManager, SessionManager>(); + ServiceCollection.AddSingleton<ISessionManager, SessionManager>(); - serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); + ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); + ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>(); - serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); + ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); - serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); + ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); - serviceCollection.AddSingleton<LiveTvDtoService>(); - serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); + ServiceCollection.AddSingleton<LiveTvDtoService>(); + ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); - serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); + ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<INotificationManager, NotificationManager>(); + ServiceCollection.AddSingleton<INotificationManager, NotificationManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); + ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); + ServiceCollection.AddSingleton<IChapterManager, ChapterManager>(); - serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); + ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); - serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); - serviceCollection.AddSingleton<ISessionContext, SessionContext>(); + ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>(); + ServiceCollection.AddSingleton<ISessionContext, SessionContext>(); - serviceCollection.AddSingleton<IAuthService, AuthService>(); + ServiceCollection.AddSingleton<IAuthService, AuthService>(); + ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); - serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); + ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); - serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>(); - serviceCollection.AddSingleton<EncodingHelper>(); + ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>(); + ServiceCollection.AddSingleton<EncodingHelper>(); - serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); + ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); - serviceCollection.AddSingleton<TranscodingJobHelper>(); + ServiceCollection.AddSingleton<TranscodingJobHelper>(); + ServiceCollection.AddScoped<MediaInfoHelper>(); + ServiceCollection.AddScoped<AudioHelper>(); + ServiceCollection.AddScoped<DynamicHlsHelper>(); } /// <summary> @@ -645,8 +665,7 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve<IMediaEncoder>(); _sessionManager = Resolve<ISessionManager>(); - _httpServer = Resolve<IHttpServer>(); - _httpClient = Resolve<IHttpClient>(); + _httpClientFactory = Resolve<IHttpClientFactory>(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); @@ -747,7 +766,6 @@ namespace Emby.Server.Implementations CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>(); CollectionFolder.ApplicationHost = this; - AuthenticatedAttribute.AuthService = Resolve<IAuthService>(); } /// <summary> @@ -767,7 +785,7 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); + _urlPrefixes = GetUrlPrefixes().ToArray(); Resolve<ILibraryManager>().AddParts( GetExports<IResolverIgnoreRule>(), @@ -800,37 +818,7 @@ namespace Emby.Server.Implementations { try { - if (plugin is IPluginAssembly assemblyPlugin) - { - var assembly = plugin.GetType().Assembly; - var assemblyName = assembly.GetName(); - var assemblyFilePath = assembly.Location; - - var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath)); - - assemblyPlugin.SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version); - - try - { - var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true); - if (idAttributes.Length > 0) - { - var attribute = (GuidAttribute)idAttributes[0]; - var assemblyId = new Guid(attribute.Value); - - assemblyPlugin.SetId(assemblyId); - } - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting plugin Id from {PluginName}.", plugin.GetType().FullName); - } - } - - if (plugin is IHasPluginConfiguration hasPluginConfiguration) - { - hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s)); - } + plugin.RegisterServices(ServiceCollection); } catch (Exception ex) { @@ -931,7 +919,7 @@ namespace Emby.Server.Implementations } } - if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) { requiresRestart = true; } @@ -1006,6 +994,119 @@ namespace Emby.Server.Implementations protected abstract void RestartInternal(); /// <summary> + /// Comparison function used in <see cref="GetPlugins" />. + /// </summary> + /// <param name="a">Item to compare.</param> + /// <param name="b">Item to compare with.</param> + /// <returns>Boolean result of the operation.</returns> + private static int VersionCompare( + (Version PluginVersion, string Name, string Path) a, + (Version PluginVersion, string Name, string Path) b) + { + int compare = string.Compare(a.Name, b.Name, true, CultureInfo.InvariantCulture); + + if (compare == 0) + { + return a.PluginVersion.CompareTo(b.PluginVersion); + } + + return compare; + } + + /// <summary> + /// Returns a list of plugins to install. + /// </summary> + /// <param name="path">Path to check.</param> + /// <param name="cleanup">True if an attempt should be made to delete old plugs.</param> + /// <returns>Enumerable list of dlls to load.</returns> + private IEnumerable<string> GetPlugins(string path, bool cleanup = true) + { + var dllList = new List<string>(); + var versions = new List<(Version PluginVersion, string Name, string Path)>(); + var directories = Directory.EnumerateDirectories(path, "*.*", SearchOption.TopDirectoryOnly); + string metafile; + + foreach (var dir in directories) + { + try + { + metafile = Path.Combine(dir, "meta.json"); + if (File.Exists(metafile)) + { + var manifest = _jsonSerializer.DeserializeFromFile<PluginManifest>(metafile); + + if (!Version.TryParse(manifest.TargetAbi, out var targetAbi)) + { + targetAbi = new Version(0, 0, 0, 1); + } + + if (!Version.TryParse(manifest.Version, out var version)) + { + version = new Version(0, 0, 0, 1); + } + + if (ApplicationVersion >= targetAbi) + { + // Only load Plugins if the plugin is built for this version or below. + versions.Add((version, manifest.Name, dir)); + } + } + else + { + // No metafile, so lets see if the folder is versioned. + metafile = dir.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)[^1]; + + int versionIndex = dir.LastIndexOf('_'); + if (versionIndex != -1 && Version.TryParse(dir.Substring(versionIndex + 1), out Version ver)) + { + // Versioned folder. + versions.Add((ver, metafile, dir)); + } + else + { + // Un-versioned folder - Add it under the path name and version 0.0.0.1. + versions.Add((new Version(0, 0, 0, 1), metafile, dir)); + } + } + } + catch + { + continue; + } + } + + string lastName = string.Empty; + versions.Sort(VersionCompare); + // Traverse backwards through the list. + // The first item will be the latest version. + for (int x = versions.Count - 1; x >= 0; x--) + { + if (!string.Equals(lastName, versions[x].Name, StringComparison.OrdinalIgnoreCase)) + { + dllList.AddRange(Directory.EnumerateFiles(versions[x].Path, "*.dll", SearchOption.AllDirectories)); + lastName = versions[x].Name; + continue; + } + + if (!string.IsNullOrEmpty(lastName) && cleanup) + { + // Attempt a cleanup of old folders. + try + { + Logger.LogDebug("Deleting {Path}", versions[x].Path); + Directory.Delete(versions[x].Path, true); + } + catch (Exception e) + { + Logger.LogWarning(e, "Unable to delete {Path}", versions[x].Path); + } + } + } + + return dllList; + } + + /// <summary> /// Gets the composable part assemblies. /// </summary> /// <returns>IEnumerable{Assembly}.</returns> @@ -1013,7 +1114,7 @@ namespace Emby.Server.Implementations { if (Directory.Exists(ApplicationPaths.PluginsPath)) { - foreach (var file in Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.AllDirectories)) + foreach (var file in GetPlugins(ApplicationPaths.PluginsPath)) { Assembly plugAss; try @@ -1127,7 +1228,8 @@ namespace Emby.Server.Implementations Id = SystemId, OperatingSystem = OperatingSystem.Id.ToString(), ServerName = FriendlyName, - LocalAddress = localAddress + LocalAddress = localAddress, + StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted }; } @@ -1289,25 +1391,17 @@ namespace Emby.Server.Implementations try { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = apiUrl, - LogErrorResponseBody = false, - BufferContent = false, - CancellationToken = cancellationToken - }, HttpMethod.Post).ConfigureAwait(false)) - { - using (var reader = new StreamReader(response.Content)) - { - var result = await reader.ReadToEndAsync().ConfigureAwait(false); - var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); - Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); - return valid; - } - } + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var result = await System.Text.Json.JsonSerializer.DeserializeAsync<string>(stream, JsonDefaults.GetOptions(), cancellationToken).ConfigureAwait(false); + var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + + _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); + Logger.LogDebug("Ping test result to {0}. Success: {1}", apiUrl, valid); + return valid; } catch (OperationCanceledException) { @@ -1385,6 +1479,20 @@ namespace Emby.Server.Implementations _plugins = list.ToArray(); } + public IEnumerable<Assembly> GetApiPluginAssemblies() + { + var assemblies = _allConcreteTypes + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + .Select(i => i.Assembly) + .Distinct(); + + foreach (var assembly in assemblies) + { + Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName); + yield return assembly; + } + } + public virtual void LaunchUrl(string url) { if (!CanLaunchWebBrowser) @@ -1415,10 +1523,6 @@ namespace Emby.Server.Implementations } } - public virtual void EnableLoopback(string appName) - { - } - private bool _disposed = false; /// <summary> diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs deleted file mode 100644 index f8108d1c2..000000000 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Browser -{ - /// <summary> - /// Assists in opening application URLs in an external browser. - /// </summary> - public static class BrowserLauncher - { - /// <summary> - /// Opens the home page of the web client. - /// </summary> - /// <param name="appHost">The app host.</param> - public static void OpenWebApp(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/web/index.html"); - } - - /// <summary> - /// Opens the swagger API page. - /// </summary> - /// <param name="appHost">The app host.</param> - public static void OpenSwaggerPage(IServerApplicationHost appHost) - { - TryOpenUrl(appHost, "/api-docs/swagger"); - } - - /// <summary> - /// Opens the specified URL in an external browser window. Any exceptions will be logged, but ignored. - /// </summary> - /// <param name="appHost">The application host.</param> - /// <param name="relativeUrl">The URL to open, relative to the server base URL.</param> - private static void TryOpenUrl(IServerApplicationHost appHost, string relativeUrl) - { - try - { - string baseUrl = appHost.GetLocalApiUrl("localhost"); - appHost.LaunchUrl(baseUrl + relativeUrl); - } - catch (Exception ex) - { - var logger = appHost.Resolve<ILogger<IServerApplicationHost>>(); - logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl); - } - } - } -} diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index d8ab1f1a1..19045b72b 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -250,21 +250,16 @@ namespace Emby.Server.Implementations.Channels var all = channels; var totalCount = all.Count; - if (query.StartIndex.HasValue) + if (query.StartIndex.HasValue || query.Limit.HasValue) { - all = all.Skip(query.StartIndex.Value).ToList(); + int startIndex = query.StartIndex ?? 0; + int count = query.Limit == null ? totalCount - startIndex : Math.Min(query.Limit.Value, totalCount - startIndex); + all = all.GetRange(startIndex, count); } - if (query.Limit.HasValue) - { - all = all.Take(query.Limit.Value).ToList(); - } - - var returnItems = all.ToArray(); - if (query.RefreshLatestChannelItems) { - foreach (var item in returnItems) + foreach (var item in all) { RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult(); } @@ -272,7 +267,7 @@ namespace Emby.Server.Implementations.Channels return new QueryResult<Channel> { - Items = returnItems, + Items = all, TotalRecordCount = totalCount }; } @@ -543,7 +538,7 @@ namespace Emby.Server.Implementations.Channels return _libraryManager.GetItemIds( new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Channel).Name }, + IncludeItemTypes = new[] { nameof(Channel) }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) } }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray(); } @@ -746,12 +741,21 @@ namespace Emby.Server.Implementations.Channels // null if came from cache if (itemsResult != null) { - var internalItems = itemsResult.Items - .Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken)) - .ToArray(); + var items = itemsResult.Items; + var itemsLen = items.Count; + var internalItems = new Guid[itemsLen]; + for (int i = 0; i < itemsLen; i++) + { + internalItems[i] = (await GetChannelItemEntityAsync( + items[i], + channelProvider, + channel.Id, + parentItem, + cancellationToken).ConfigureAwait(false)).Id; + } var existingIds = _libraryManager.GetItemIds(query); - var deadIds = existingIds.Except(internalItems.Select(i => i.Id)) + var deadIds = existingIds.Except(internalItems) .ToArray(); foreach (var deadId in deadIds) @@ -881,7 +885,7 @@ namespace Emby.Server.Implementations.Channels } catch (Exception ex) { - _logger.LogError(ex, "Error writing to channel cache file: {path}", path); + _logger.LogError(ex, "Error writing to channel cache file: {Path}", path); } } @@ -963,7 +967,7 @@ namespace Emby.Server.Implementations.Channels return item; } - private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) + private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; @@ -1165,7 +1169,7 @@ namespace Emby.Server.Implementations.Channels } else if (forceUpdate) { - item.UpdateToRepository(ItemUpdateType.None, cancellationToken); + await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); } if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media) diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index eeb49b8fe..2391eed42 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.Channels var uninstalledChannels = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Channel).Name }, + IncludeItemTypes = new[] { nameof(Channel) }, ExcludeItemIds = installedChannelIds.ToArray() }); diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index ac2edc1e2..3011a37e3 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections } /// <inheritdoc /> - public BoxSet CreateCollection(CollectionCreationOptions options) + public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options) { var name = options.Name; @@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections // This could cause it to get re-resolved as a plain folder var folderName = _fileSystem.GetValidFilename(name) + " [boxset]"; - var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult(); + var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false); if (parentFolder == null) { @@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections if (options.ItemIdList.Length > 0) { - AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - // The initial adding of items is going to create a local metadata file - // This will cause internet metadata to be skipped as a result - MetadataRefreshMode = MetadataRefreshMode.FullRefresh - }); + await AddToCollectionAsync( + collection.Id, + options.ItemIdList.Select(x => new Guid(x)), + false, + new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + { + // The initial adding of items is going to create a local metadata file + // This will cause internet metadata to be skipped as a result + MetadataRefreshMode = MetadataRefreshMode.FullRefresh + }).ConfigureAwait(false); } else { @@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections } /// <inheritdoc /> - public void AddToCollection(Guid collectionId, IEnumerable<string> ids) - { - AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - } + public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids) + => AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - /// <inheritdoc /> - public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids) - { - AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem))); - } - - private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) + private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; if (collection == null) @@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections foreach (var id in ids) { - var guidId = new Guid(id); - var item = _libraryManager.GetItemById(guidId); + var item = _libraryManager.GetItemById(id); if (item == null) { throw new ArgumentException("No item exists with the supplied Id"); } - if (!currentLinkedChildrenIds.Contains(guidId)) + if (!currentLinkedChildrenIds.Contains(id)) { itemList.Add(item); @@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections collection.UpdateRatingToItems(linkedChildrenList); - collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); refreshOptions.ForceSave = true; _providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High); @@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections } /// <inheritdoc /> - public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds) - { - RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i))); - } - - /// <inheritdoc /> - public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds) + public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds) { var collection = _libraryManager.GetItemById(collectionId) as BoxSet; @@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray(); } - collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); _providerManager.QueueRefresh( collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)) diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index a15295fca..f05a30a89 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -2,11 +2,11 @@ using System; using System.Globalization; using System.IO; using Emby.Server.Implementations.AppBase; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 64ccff53b..cd9dbb1bd 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,10 +15,10 @@ namespace Emby.Server.Implementations public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> { { HostWebClientKey, bool.TrueString }, - { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, - { PlaylistsAllowDuplicatesKey, bool.TrueString }, + { PlaylistsAllowDuplicatesKey, bool.FalseString }, { BindToUnixSocketKey, bool.FalseString } }; } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 8a3716380..8c756a7f4 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -143,12 +143,22 @@ namespace Emby.Server.Implementations.Data public IStatement PrepareStatement(IDatabaseConnection connection, string sql) => connection.PrepareStatement(sql); - public IEnumerable<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql) - => sql.Select(connection.PrepareStatement); + public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql) + { + int len = sql.Count; + IStatement[] statements = new IStatement[len]; + for (int i = 0; i < len; i++) + { + statements[i] = connection.PrepareStatement(sql[i]); + } + + return statements; + } protected bool TableExists(ManagedConnection connection, string name) { - return connection.RunInTransaction(db => + return connection.RunInTransaction( + db => { using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master")) { diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 716e5071d..70a6df977 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -234,7 +234,9 @@ namespace Emby.Server.Implementations.Data { if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam)) { - bindParam.Bind(value.ToByteArray()); + Span<byte> byteValue = stackalloc byte[16]; + value.TryWriteBytes(byteValue); + bindParam.Bind(byteValue); } else { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d11e5e62e..acb75e9b8 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -138,7 +138,6 @@ namespace Emby.Server.Implementations.Data "pragma shrink_memory" }; - string[] postQueries = { // obsolete @@ -220,7 +219,8 @@ namespace Emby.Server.Implementations.Data { connection.RunQueries(queries); - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var existingColumnNames = GetColumnNames(db, "AncestorIds"); AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); @@ -496,7 +496,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var saveImagesStatement = base.PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { @@ -547,7 +548,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { SaveItemsInTranscation(db, tuples); }, TransactionMode); @@ -560,7 +562,7 @@ namespace Emby.Server.Implementations.Data { SaveItemCommandText, "delete from AncestorIds where ItemId=@ItemId" - }).ToList(); + }); using (var saveItemStatement = statements[0]) using (var deleteAncestorsStatement = statements[1]) @@ -2033,7 +2035,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { // First delete chapters db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", idBlob); @@ -2264,7 +2267,6 @@ namespace Emby.Server.Implementations.Data return query.IncludeItemTypes.Contains("Trailer", StringComparer.OrdinalIgnoreCase); } - private static readonly HashSet<string> _artistExcludeParentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Series", @@ -2923,9 +2925,10 @@ namespace Emby.Server.Implementations.Data var result = new QueryResult<BaseItem>(); using (var connection = GetConnection(true)) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { - var statements = PrepareAll(db, statementTexts).ToList(); + var statements = PrepareAll(db, statementTexts); if (!isReturningZeroItems) { @@ -2963,7 +2966,7 @@ namespace Emby.Server.Implementations.Data if (query.EnableTotalRecordCount) { - using (var statement = statements[statements.Count - 1]) + using (var statement = statements[statements.Length - 1]) { if (EnableJoinUserData(query)) { @@ -3292,7 +3295,6 @@ namespace Emby.Server.Implementations.Data } } - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; var statementTexts = new List<string>(); @@ -3327,9 +3329,10 @@ namespace Emby.Server.Implementations.Data var result = new QueryResult<Guid>(); using (var connection = GetConnection(true)) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { - var statements = PrepareAll(db, statementTexts).ToList(); + var statements = PrepareAll(db, statementTexts); if (!isReturningZeroItems) { @@ -3355,7 +3358,7 @@ namespace Emby.Server.Implementations.Data if (query.EnableTotalRecordCount) { - using (var statement = statements[statements.Count - 1]) + using (var statement = statements[statements.Length - 1]) { if (EnableJoinUserData(query)) { @@ -3718,26 +3721,31 @@ namespace Emby.Server.Implementations.Data statement?.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); } + StringBuilder clauseBuilder = new StringBuilder(); + const string Or = " OR "; + var trailerTypes = query.TrailerTypes; int trailerTypesLen = trailerTypes.Length; if (trailerTypesLen > 0) { - const string Or = " OR "; - StringBuilder clause = new StringBuilder("(", trailerTypesLen * 32); + clauseBuilder.Append('('); + for (int i = 0; i < trailerTypesLen; i++) { var paramName = "@TrailerTypes" + i; - clause.Append("TrailerTypes like ") + clauseBuilder.Append("TrailerTypes like ") .Append(paramName) .Append(Or); statement?.TryBind(paramName, "%" + trailerTypes[i] + "%"); } // Remove last " OR " - clause.Length -= Or.Length; - clause.Append(')'); + clauseBuilder.Length -= Or.Length; + clauseBuilder.Append(')'); + + whereClauses.Add(clauseBuilder.ToString()); - whereClauses.Add(clause.ToString()); + clauseBuilder.Length = 0; } if (query.IsAiring.HasValue) @@ -3757,23 +3765,35 @@ namespace Emby.Server.Implementations.Data } } - if (query.PersonIds.Length > 0) + int personIdsLen = query.PersonIds.Length; + if (personIdsLen > 0) { // TODO: Should this query with CleanName ? - var clauses = new List<string>(); - var index = 0; - foreach (var personId in query.PersonIds) + clauseBuilder.Append('('); + + Span<byte> idBytes = stackalloc byte[16]; + for (int i = 0; i < personIdsLen; i++) { - var paramName = "@PersonId" + index; + string paramName = "@PersonId" + i; + clauseBuilder.Append("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=") + .Append(paramName) + .Append("))) OR "); - clauses.Add("(guid in (select itemid from People where Name = (select Name from TypedBaseItems where guid=" + paramName + ")))"); - statement?.TryBind(paramName, personId.ToByteArray()); - index++; + if (statement != null) + { + query.PersonIds[i].TryWriteBytes(idBytes); + statement.TryBind(paramName, idBytes); + } } - var clause = "(" + string.Join(" OR ", clauses) + ")"; - whereClauses.Add(clause); + // Remove last " OR " + clauseBuilder.Length -= Or.Length; + clauseBuilder.Append(')'); + + whereClauses.Add(clauseBuilder.ToString()); + + clauseBuilder.Length = 0; } if (!string.IsNullOrWhiteSpace(query.Person)) @@ -3894,7 +3914,7 @@ namespace Emby.Server.Implementations.Data if (query.IsPlayed.HasValue) { // We should probably figure this out for all folders, but for right now, this is the only place where we need it - if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], typeof(Series).Name, StringComparison.OrdinalIgnoreCase)) + if (query.IncludeItemTypes.Length == 1 && string.Equals(query.IncludeItemTypes[0], nameof(Series), StringComparison.OrdinalIgnoreCase)) { if (query.IsPlayed.Value) { @@ -4308,7 +4328,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add("ProductionYear=@Years"); if (statement != null) { - statement.TryBind("@Years", query.Years[0].ToString()); + statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture)); } } else if (query.Years.Length > 1) @@ -4560,13 +4580,13 @@ namespace Emby.Server.Implementations.Data if (query.AncestorIds.Length > 1) { var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'")); - whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause)); } if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey)) { var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; - whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); + whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); if (statement != null) { statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); @@ -4735,29 +4755,29 @@ namespace Emby.Server.Implementations.Data { var list = new List<string>(); - if (IsTypeInQuery(typeof(Person).Name, query)) + if (IsTypeInQuery(nameof(Person), query)) { - list.Add(typeof(Person).Name); + list.Add(nameof(Person)); } - if (IsTypeInQuery(typeof(Genre).Name, query)) + if (IsTypeInQuery(nameof(Genre), query)) { - list.Add(typeof(Genre).Name); + list.Add(nameof(Genre)); } - if (IsTypeInQuery(typeof(MusicGenre).Name, query)) + if (IsTypeInQuery(nameof(MusicGenre), query)) { - list.Add(typeof(MusicGenre).Name); + list.Add(nameof(MusicGenre)); } - if (IsTypeInQuery(typeof(MusicArtist).Name, query)) + if (IsTypeInQuery(nameof(MusicArtist), query)) { - list.Add(typeof(MusicArtist).Name); + list.Add(nameof(MusicArtist)); } - if (IsTypeInQuery(typeof(Studio).Name, query)) + if (IsTypeInQuery(nameof(Studio), query)) { - list.Add(typeof(Studio).Name); + list.Add(nameof(Studio)); } return list; @@ -4812,12 +4832,12 @@ namespace Emby.Server.Implementations.Data var types = new[] { - typeof(Episode).Name, - typeof(Video).Name, - typeof(Movie).Name, - typeof(MusicVideo).Name, - typeof(Series).Name, - typeof(Season).Name + nameof(Episode), + nameof(Video), + nameof(Movie), + nameof(MusicVideo), + nameof(Series), + nameof(Season) }; if (types.Any(i => query.IncludeItemTypes.Contains(i, StringComparer.OrdinalIgnoreCase))) @@ -4885,7 +4905,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { connection.ExecuteAll(sql); }, TransactionMode); @@ -4936,7 +4957,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var idBlob = id.ToByteArray(); @@ -4980,26 +5002,33 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var commandText = "select Distinct Name from People"; + var commandText = new StringBuilder("select Distinct p.Name from People p"); + + if (query.User != null && query.IsFavorite.HasValue) + { + commandText.Append(" LEFT JOIN TypedBaseItems tbi ON tbi.Name=p.Name AND tbi.Type='"); + commandText.Append(typeof(Person).FullName); + commandText.Append("' LEFT JOIN UserDatas ON tbi.UserDataKey=key AND userId=@UserId"); + } var whereClauses = GetPeopleWhereClauses(query, null); if (whereClauses.Count != 0) { - commandText += " where " + string.Join(" AND ", whereClauses); + commandText.Append(" where ").Append(string.Join(" AND ", whereClauses)); } - commandText += " order by ListOrder"; + commandText.Append(" order by ListOrder"); if (query.Limit > 0) { - commandText += " LIMIT " + query.Limit; + commandText.Append(" LIMIT ").Append(query.Limit); } using (var connection = GetConnection(true)) { var list = new List<string>(); - using (var statement = PrepareStatement(connection, commandText)) + using (var statement = PrepareStatement(connection, commandText.ToString())) { // Run this again to bind the params GetPeopleWhereClauses(query, statement); @@ -5065,19 +5094,13 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (!query.ItemId.Equals(Guid.Empty)) { whereClauses.Add("ItemId=@ItemId"); - if (statement != null) - { - statement.TryBind("@ItemId", query.ItemId.ToByteArray()); - } + statement?.TryBind("@ItemId", query.ItemId.ToByteArray()); } if (!query.AppearsInItemId.Equals(Guid.Empty)) { - whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); - if (statement != null) - { - statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray()); - } + whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); + statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray()); } var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); @@ -5085,10 +5108,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (queryPersonTypes.Count == 1) { whereClauses.Add("PersonType=@PersonType"); - if (statement != null) - { - statement.TryBind("@PersonType", queryPersonTypes[0]); - } + statement?.TryBind("@PersonType", queryPersonTypes[0]); } else if (queryPersonTypes.Count > 1) { @@ -5102,10 +5122,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (queryExcludePersonTypes.Count == 1) { whereClauses.Add("PersonType<>@PersonType"); - if (statement != null) - { - statement.TryBind("@PersonType", queryExcludePersonTypes[0]); - } + statement?.TryBind("@PersonType", queryExcludePersonTypes[0]); } else if (queryExcludePersonTypes.Count > 1) { @@ -5117,19 +5134,24 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (query.MaxListOrder.HasValue) { whereClauses.Add("ListOrder<=@MaxListOrder"); - if (statement != null) - { - statement.TryBind("@MaxListOrder", query.MaxListOrder.Value); - } + statement?.TryBind("@MaxListOrder", query.MaxListOrder.Value); } if (!string.IsNullOrWhiteSpace(query.NameContains)) { - whereClauses.Add("Name like @NameContains"); - if (statement != null) - { - statement.TryBind("@NameContains", "%" + query.NameContains + "%"); - } + whereClauses.Add("p.Name like @NameContains"); + statement?.TryBind("@NameContains", "%" + query.NameContains + "%"); + } + + if (query.IsFavorite.HasValue) + { + whereClauses.Add("isFavorite=@IsFavorite"); + statement?.TryBind("@IsFavorite", query.IsFavorite.Value); + } + + if (query.User != null) + { + statement?.TryBind("@UserId", query.User.InternalId); } return whereClauses; @@ -5149,7 +5171,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type CheckDisposed(); - var itemIdBlob = itemId.ToByteArray(); + Span<byte> itemIdBlob = stackalloc byte[16]; + itemId.TryWriteBytes(itemIdBlob); // First delete deleteAncestorsStatement.Reset(); @@ -5165,14 +5188,15 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type for (var i = 0; i < ancestorIds.Count; i++) { - if (i > 0) - { - insertText.Append(','); - } - - insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture)); + insertText.AppendFormat( + CultureInfo.InvariantCulture, + "(@ItemId, @AncestorId{0}, @AncestorIdText{0}),", + i.ToString(CultureInfo.InvariantCulture)); } + // Remove last , + insertText.Length--; + using (var statement = PrepareStatement(db, insertText.ToString())) { statement.TryBind("@ItemId", itemIdBlob); @@ -5182,8 +5206,9 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type var index = i.ToString(CultureInfo.InvariantCulture); var ancestorId = ancestorIds[i]; + ancestorId.TryWriteBytes(itemIdBlob); - statement.TryBind("@AncestorId" + index, ancestorId.ToByteArray()); + statement.TryBind("@AncestorId" + index, itemIdBlob); statement.TryBind("@AncestorIdText" + index, ancestorId.ToString("N", CultureInfo.InvariantCulture)); } @@ -5340,7 +5365,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type itemCountColumns = new Dictionary<string, string>() { - { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes"} + { "itemTypes", "(" + itemCountColumnQuery + ") as itemTypes" } }; } @@ -5395,6 +5420,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type NameStartsWithOrGreater = query.NameStartsWithOrGreater, Tags = query.Tags, OfficialRatings = query.OfficialRatings, + StudioIds = query.StudioIds, GenreIds = query.GenreIds, Genres = query.Genres, Years = query.Years, @@ -5463,7 +5489,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts).ToList(); + var statements = PrepareAll(db, statementTexts); if (!isReturningZeroItems) { @@ -5514,7 +5540,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type + GetJoinUserDataText(query) + whereText; - using (var statement = statements[statements.Count - 1]) + using (var statement = statements[statements.Length - 1]) { statement.TryBind("@SelectType", returnType); if (EnableJoinUserData(query)) @@ -5727,7 +5753,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = itemId.ToByteArray(); @@ -5881,7 +5908,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = id.ToByteArray(); @@ -5987,7 +6015,6 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type } } - /// <summary> /// Gets the chapter. /// </summary> @@ -6216,7 +6243,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var itemIdBlob = id.ToByteArray(); diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 4a78aac8e..2c4e8e0fc 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -44,7 +44,8 @@ namespace Emby.Server.Implementations.Data var users = userDatasTableExists ? null : userManager.Users; - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { db.ExecuteAll(string.Join(";", new[] { @@ -178,7 +179,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { SaveUserData(db, internalUserId, key, userData); }, TransactionMode); @@ -246,7 +248,8 @@ namespace Emby.Server.Implementations.Data using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { foreach (var userItemData in userDataList) { diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index cc4b407f5..f98c694c4 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -7,13 +7,13 @@ using System.IO; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Security; using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index f2c7118fe..73502c2c9 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name); + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name); } } @@ -465,10 +465,9 @@ namespace Emby.Server.Implementations.Dto { var parentAlbumIds = _libraryManager.GetItemIds(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + IncludeItemTypes = new[] { nameof(MusicAlbum) }, Name = item.Album, Limit = 1 - }); if (parentAlbumIds.Count > 0) @@ -1139,6 +1138,7 @@ namespace Emby.Server.Implementations.Dto if (episodeSeries != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); + AttachPrimaryImageAspectRatio(dto, episodeSeries); } } @@ -1185,6 +1185,7 @@ namespace Emby.Server.Implementations.Dto if (series != null) { dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); + AttachPrimaryImageAspectRatio(dto, series); } } } @@ -1431,7 +1432,7 @@ namespace Emby.Server.Implementations.Dto return null; } - return width / height; + return (double)width / height; } } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 1adef68aa..c762aa0b8 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="IPNetwork2" Version="2.5.211" /> + <PackageReference Include="IPNetwork2" Version="2.5.226" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> @@ -32,16 +32,16 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" /> - <PackageReference Include="Mono.Nat" Version="2.0.2" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.9" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.9" /> + <PackageReference Include="Mono.Nat" Version="3.0.0" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" /> <PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> - <PackageReference Include="DotNet.Glob" Version="3.0.9" /> + <PackageReference Include="DotNet.Glob" Version="3.1.0" /> </ItemGroup> <ItemGroup> diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs index 9fce49425..2e8cc76d2 100644 --- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs @@ -7,11 +7,11 @@ using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Events; using Microsoft.Extensions.Logging; using Mono.Nat; diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 1deef7f72..ff64e217a 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -15,7 +16,7 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -105,7 +106,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - _sessionManager.SendMessageToAdminSessions("RefreshProgress", dict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, dict, CancellationToken.None); } catch { @@ -123,7 +124,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - _sessionManager.SendMessageToAdminSessions("RefreshProgress", collectionFolderDict, CancellationToken.None); + _sessionManager.SendMessageToAdminSessions(SessionMessageType.RefreshProgress, collectionFolderDict, CancellationToken.None); } catch { @@ -345,7 +346,7 @@ namespace Emby.Server.Implementations.EntryPoints try { - await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "LibraryChanged", info, cancellationToken).ConfigureAwait(false); + await _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.LibraryChanged, info, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 632735910..824bb85f4 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -5,10 +5,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints @@ -43,27 +45,27 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) { - await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.SeriesTimerCreated, e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e) { - await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.TimerCreated, e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) { - await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.SeriesTimerCancelled, e.Argument).ConfigureAwait(false); } - private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e) { - await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false); + await SendMessage(SessionMessageType.TimerCancelled, e.Argument).ConfigureAwait(false); } - private async Task SendMessage(string name, TimerEventInfo info) + private async Task SendMessage(SessionMessageType name, TimerEventInfo info) { var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList(); diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs deleted file mode 100644 index 826d4d8dc..000000000 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Data.Entities; -using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Updates; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Updates; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// <summary> - /// Class WebSocketEvents. - /// </summary> - public class ServerEventNotifier : IServerEntryPoint - { - /// <summary> - /// The user manager. - /// </summary> - private readonly IUserManager _userManager; - - /// <summary> - /// The installation manager. - /// </summary> - private readonly IInstallationManager _installationManager; - - /// <summary> - /// The kernel. - /// </summary> - private readonly IServerApplicationHost _appHost; - - /// <summary> - /// The task manager. - /// </summary> - private readonly ITaskManager _taskManager; - - private readonly ISessionManager _sessionManager; - - /// <summary> - /// Initializes a new instance of the <see cref="ServerEventNotifier"/> class. - /// </summary> - /// <param name="appHost">The application host.</param> - /// <param name="userManager">The user manager.</param> - /// <param name="installationManager">The installation manager.</param> - /// <param name="taskManager">The task manager.</param> - /// <param name="sessionManager">The session manager.</param> - public ServerEventNotifier( - IServerApplicationHost appHost, - IUserManager userManager, - IInstallationManager installationManager, - ITaskManager taskManager, - ISessionManager sessionManager) - { - _userManager = userManager; - _installationManager = installationManager; - _appHost = appHost; - _taskManager = taskManager; - _sessionManager = sessionManager; - } - - /// <inheritdoc /> - public Task RunAsync() - { - _userManager.OnUserDeleted += OnUserDeleted; - _userManager.OnUserUpdated += OnUserUpdated; - - _appHost.HasPendingRestartChanged += OnHasPendingRestartChanged; - - _installationManager.PluginUninstalled += OnPluginUninstalled; - _installationManager.PackageInstalling += OnPackageInstalling; - _installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled; - _installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted; - _installationManager.PackageInstallationFailed += OnPackageInstallationFailed; - - _taskManager.TaskCompleted += OnTaskCompleted; - - return Task.CompletedTask; - } - - private async void OnPackageInstalling(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationCancelled(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationCompleted(object sender, InstallationInfo e) - { - await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false); - } - - private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) - { - await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false); - } - - private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) - { - await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false); - } - - /// <summary> - /// Installations the manager_ plugin uninstalled. - /// </summary> - /// <param name="sender">The sender.</param> - /// <param name="e">The e.</param> - private async void OnPluginUninstalled(object sender, IPlugin e) - { - await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false); - } - - /// <summary> - /// Handles the HasPendingRestartChanged event of the kernel control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - private async void OnHasPendingRestartChanged(object sender, EventArgs e) - { - await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false); - } - - /// <summary> - /// Users the manager_ user updated. - /// </summary> - /// <param name="sender">The sender.</param> - /// <param name="e">The e.</param> - private async void OnUserUpdated(object sender, GenericEventArgs<User> e) - { - var dto = _userManager.GetUserDto(e.Argument); - - await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false); - } - - /// <summary> - /// Users the manager_ user deleted. - /// </summary> - /// <param name="sender">The sender.</param> - /// <param name="e">The e.</param> - private async void OnUserDeleted(object sender, GenericEventArgs<User> e) - { - await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false); - } - - private async Task SendMessageToAdminSessions<T>(string name, T data) - { - try - { - await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) - { - } - } - - private async Task SendMessageToUserSession<T>(User user, string name, T data) - { - try - { - await _sessionManager.SendMessageToUserSessions( - new List<Guid> { user.Id }, - name, - data, - CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) - { - } - } - - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (dispose) - { - _userManager.OnUserDeleted -= OnUserDeleted; - _userManager.OnUserUpdated -= OnUserUpdated; - - _installationManager.PluginUninstalled -= OnPluginUninstalled; - _installationManager.PackageInstalling -= OnPackageInstalling; - _installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled; - _installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted; - _installationManager.PackageInstallationFailed -= OnPackageInstallationFailed; - - _appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged; - - _taskManager.TaskCompleted -= OnTaskCompleted; - } - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs deleted file mode 100644 index 2e738deeb..000000000 --- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Threading.Tasks; -using Emby.Server.Implementations.Browser; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Extensions; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Configuration; - -namespace Emby.Server.Implementations.EntryPoints -{ - /// <summary> - /// Class StartupWizard. - /// </summary> - public sealed class StartupWizard : IServerEntryPoint - { - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _appConfig; - private readonly IServerConfigurationManager _config; - private readonly IStartupOptions _startupOptions; - - /// <summary> - /// Initializes a new instance of the <see cref="StartupWizard"/> class. - /// </summary> - /// <param name="appHost">The application host.</param> - /// <param name="appConfig">The application configuration.</param> - /// <param name="config">The configuration manager.</param> - /// <param name="startupOptions">The application startup options.</param> - public StartupWizard( - IServerApplicationHost appHost, - IConfiguration appConfig, - IServerConfigurationManager config, - IStartupOptions startupOptions) - { - _appHost = appHost; - _appConfig = appConfig; - _config = config; - _startupOptions = startupOptions; - } - - /// <inheritdoc /> - public Task RunAsync() - { - Run(); - return Task.CompletedTask; - } - - private void Run() - { - if (!_appHost.CanLaunchWebBrowser) - { - return; - } - - // Always launch the startup wizard if possible when it has not been completed - if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - return; - } - - // Do nothing if the web app is configured to not run automatically - if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp) - { - return; - } - - // Launch the swagger page if the web client is not hosted, otherwise open the web client - if (_appConfig.HostWebClient()) - { - BrowserLauncher.OpenWebApp(_appHost); - } - else - { - BrowserLauncher.OpenSwaggerPage(_appHost); - } - } - - /// <inheritdoc /> - public void Dispose() - { - } - } -} diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 3618b88c5..1989e9ed2 100644 --- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -28,7 +28,6 @@ namespace Emby.Server.Implementations.EntryPoints private readonly object _syncLock = new object(); private Timer _updateTimer; - public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager) { _userDataManager = userDataManager; @@ -116,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken) { - return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, "UserDataChanged", () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); + return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken); } private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems) diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs deleted file mode 100644 index 25adc5812..000000000 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MediaBrowser.Common; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Net; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpClientManager -{ - /// <summary> - /// Class HttpClientManager. - /// </summary> - public class HttpClientManager : IHttpClient - { - private readonly ILogger<HttpClientManager> _logger; - private readonly IApplicationPaths _appPaths; - private readonly IFileSystem _fileSystem; - private readonly IApplicationHost _appHost; - - /// <summary> - /// Holds a dictionary of http clients by host. Use GetHttpClient(host) to retrieve or create a client for web requests. - /// DON'T dispose it after use. - /// </summary> - /// <value>The HTTP clients.</value> - private readonly ConcurrentDictionary<string, HttpClient> _httpClients = new ConcurrentDictionary<string, HttpClient>(); - - /// <summary> - /// Initializes a new instance of the <see cref="HttpClientManager" /> class. - /// </summary> - public HttpClientManager( - IApplicationPaths appPaths, - ILogger<HttpClientManager> logger, - IFileSystem fileSystem, - IApplicationHost appHost) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _fileSystem = fileSystem; - _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); - _appHost = appHost; - } - - /// <summary> - /// Gets the correct http client for the given url. - /// </summary> - /// <param name="url">The url.</param> - /// <returns>HttpClient.</returns> - private HttpClient GetHttpClient(string url) - { - var key = GetHostFromUrl(url); - - if (!_httpClients.TryGetValue(key, out var client)) - { - client = new HttpClient() - { - BaseAddress = new Uri(url) - }; - - _httpClients.TryAdd(key, client); - } - - return client; - } - - private HttpRequestMessage GetRequestMessage(HttpRequestOptions options, HttpMethod method) - { - string url = options.Url; - var uriAddress = new Uri(url); - string userInfo = uriAddress.UserInfo; - if (!string.IsNullOrWhiteSpace(userInfo)) - { - _logger.LogWarning("Found userInfo in url: {0} ... url: {1}", userInfo, url); - url = url.Replace(userInfo + '@', string.Empty, StringComparison.Ordinal); - } - - var request = new HttpRequestMessage(method, url); - - foreach (var header in options.RequestHeaders) - { - request.Headers.TryAddWithoutValidation(header.Key, header.Value); - } - - if (options.EnableDefaultUserAgent - && !request.Headers.TryGetValues(HeaderNames.UserAgent, out _)) - { - request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent); - } - - switch (options.DecompressionMethod) - { - case CompressionMethods.Deflate | CompressionMethods.Gzip: - request.Headers.Add(HeaderNames.AcceptEncoding, new[] { "gzip", "deflate" }); - break; - case CompressionMethods.Deflate: - request.Headers.Add(HeaderNames.AcceptEncoding, "deflate"); - break; - case CompressionMethods.Gzip: - request.Headers.Add(HeaderNames.AcceptEncoding, "gzip"); - break; - default: - break; - } - - if (options.EnableKeepAlive) - { - request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); - } - - // request.Headers.Add(HeaderNames.CacheControl, "no-cache"); - - /* - if (!string.IsNullOrWhiteSpace(userInfo)) - { - var parts = userInfo.Split(':'); - if (parts.Length == 2) - { - request.Headers.Add(HeaderNames., GetCredential(url, parts[0], parts[1]); - } - } - */ - - return request; - } - - /// <summary> - /// Gets the response internal. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - public Task<HttpResponseInfo> GetResponse(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Get); - - /// <summary> - /// Performs a GET request and returns the resulting stream. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{Stream}.</returns> - public async Task<Stream> Get(HttpRequestOptions options) - { - var response = await GetResponse(options).ConfigureAwait(false); - return response.Content; - } - - /// <summary> - /// send as an asynchronous operation. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="httpMethod">The HTTP method.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - public Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, string httpMethod) - => SendAsync(options, new HttpMethod(httpMethod)); - - /// <summary> - /// send as an asynchronous operation. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="httpMethod">The HTTP method.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - public async Task<HttpResponseInfo> SendAsync(HttpRequestOptions options, HttpMethod httpMethod) - { - if (options.CacheMode == CacheMode.None) - { - return await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); - } - - var url = options.Url; - var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); - - var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); - - var response = GetCachedResponse(responseCachePath, options.CacheLength, url); - if (response != null) - { - return response; - } - - response = await SendAsyncInternal(options, httpMethod).ConfigureAwait(false); - - if (response.StatusCode == HttpStatusCode.OK) - { - await CacheResponse(response, responseCachePath).ConfigureAwait(false); - } - - return response; - } - - private HttpResponseInfo GetCachedResponse(string responseCachePath, TimeSpan cacheLength, string url) - { - if (File.Exists(responseCachePath) - && _fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) - { - var stream = new FileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, IODefaults.FileStreamBufferSize, true); - - return new HttpResponseInfo - { - ResponseUrl = url, - Content = stream, - StatusCode = HttpStatusCode.OK, - ContentLength = stream.Length - }; - } - - return null; - } - - private async Task CacheResponse(HttpResponseInfo response, string responseCachePath) - { - Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); - - using (var fileStream = new FileStream( - responseCachePath, - FileMode.Create, - FileAccess.Write, - FileShare.None, - IODefaults.FileStreamBufferSize, - true)) - { - await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); - - response.Content.Position = 0; - } - } - - private async Task<HttpResponseInfo> SendAsyncInternal(HttpRequestOptions options, HttpMethod httpMethod) - { - ValidateParams(options); - - options.CancellationToken.ThrowIfCancellationRequested(); - - var client = GetHttpClient(options.Url); - - var httpWebRequest = GetRequestMessage(options, httpMethod); - - if (!string.IsNullOrEmpty(options.RequestContent) - || httpMethod == HttpMethod.Post) - { - if (options.RequestContent != null) - { - httpWebRequest.Content = new StringContent( - options.RequestContent, - null, - options.RequestContentType); - } - else - { - httpWebRequest.Content = new ByteArrayContent(Array.Empty<byte>()); - } - } - - options.CancellationToken.ThrowIfCancellationRequested(); - - var response = await client.SendAsync( - httpWebRequest, - options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, - options.CancellationToken).ConfigureAwait(false); - - await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); - - options.CancellationToken.ThrowIfCancellationRequested(); - - var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); - return new HttpResponseInfo(response.Headers, response.Content.Headers) - { - Content = stream, - StatusCode = response.StatusCode, - ContentType = response.Content.Headers.ContentType?.MediaType, - ContentLength = response.Content.Headers.ContentLength, - ResponseUrl = response.Content.Headers.ContentLocation?.ToString() - }; - } - - /// <inheritdoc /> - public Task<HttpResponseInfo> Post(HttpRequestOptions options) - => SendAsync(options, HttpMethod.Post); - - private void ValidateParams(HttpRequestOptions options) - { - if (string.IsNullOrEmpty(options.Url)) - { - throw new ArgumentNullException(nameof(options)); - } - } - - /// <summary> - /// Gets the host from URL. - /// </summary> - /// <param name="url">The URL.</param> - /// <returns>System.String.</returns> - private static string GetHostFromUrl(string url) - { - var index = url.IndexOf("://", StringComparison.OrdinalIgnoreCase); - - if (index != -1) - { - url = url.Substring(index + 3); - var host = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - - if (!string.IsNullOrWhiteSpace(host)) - { - return host; - } - } - - return url; - } - - private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) - { - if (response.IsSuccessStatusCode) - { - return; - } - - if (options.LogErrorResponseBody) - { - string msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - _logger.LogError("HTTP request failed with message: {Message}", msg); - } - - throw new HttpException(response.ReasonPhrase) - { - StatusCode = response.StatusCode - }; - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs deleted file mode 100644 index 6fce8de44..000000000 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ /dev/null @@ -1,250 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class FileWriter : IHttpResult - { - private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - - private static readonly string[] _skipLogExtensions = { - ".js", - ".html", - ".css" - }; - - private readonly IStreamHelper _streamHelper; - private readonly ILogger _logger; - - /// <summary> - /// The _options. - /// </summary> - private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); - - /// <summary> - /// The _requested ranges. - /// </summary> - private List<KeyValuePair<long, long?>> _requestedRanges; - - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - _streamHelper = streamHelper; - - Path = path; - _logger = logger; - RangeHeader = rangeHeader; - - Headers[HeaderNames.ContentType] = contentType; - - TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers[HeaderNames.AcceptRanges] = "bytes"; - - if (string.IsNullOrWhiteSpace(rangeHeader)) - { - Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); - StatusCode = HttpStatusCode.OK; - } - else - { - StatusCode = HttpStatusCode.PartialContent; - SetRangeValues(); - } - - FileShare = FileShare.Read; - Cookies = new List<Cookie>(); - } - - private string RangeHeader { get; set; } - - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - - private long RangeEnd { get; set; } - - private long RangeLength { get; set; } - - public long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public List<Cookie> Cookies { get; private set; } - - public FileShare FileShare { get; set; } - - /// <summary> - /// Gets the options. - /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Headers => _options; - - public string Path { get; set; } - - /// <summary> - /// Gets the requested ranges. - /// </summary> - /// <value>The requested ranges.</value> - protected List<KeyValuePair<long, long?>> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List<KeyValuePair<long, long?>>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], UsCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], UsCulture); - } - - _requestedRanges.Add(new KeyValuePair<long, long?>(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// <summary> - /// Sets the range values. - /// </summary> - private void SetRangeValues() - { - var requestedRange = RequestedRanges[0]; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentLength] = lengthString; - var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - Headers[HeaderNames.ContentRange] = rangeString; - - _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); - } - - public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - var path = Path; - var offset = RangeStart; - var count = RangeLength; - - if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) - { - var extension = System.IO.Path.GetExtension(path); - - if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) - { - _logger.LogDebug("Transmit file {0}", path); - } - - offset = 0; - count = 0; - } - - await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); - } - finally - { - OnComplete?.Invoke(); - } - } - - public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken) - { - var fileOptions = FileOptions.SequentialScan; - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fileOptions |= FileOptions.Asynchronous; - } - - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs deleted file mode 100644 index dafdd5b7b..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ /dev/null @@ -1,766 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Sockets; -using System.Net.WebSockets; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using ServiceStack.Text.Jsv; - -namespace Emby.Server.Implementations.HttpServer -{ - public class HttpListenerHost : IHttpServer - { - /// <summary> - /// The key for a setting that specifies the default redirect path - /// to use for requests where the URL base prefix is invalid or missing. - /// </summary> - public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; - - private readonly ILogger<HttpListenerHost> _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; - private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; - private readonly Func<Type, Func<string, object>> _funcParseFn; - private readonly string _defaultRedirectPath; - private readonly string _baseUrlPrefix; - - private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); - private readonly IHostEnvironment _hostEnvironment; - - private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); - private bool _disposed = false; - - public HttpListenerHost( - IServerApplicationHost applicationHost, - ILogger<HttpListenerHost> logger, - IServerConfigurationManager config, - IConfiguration configuration, - INetworkManager networkManager, - IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer, - ILocalizationManager localizationManager, - ServiceController serviceController, - IHostEnvironment hostEnvironment, - ILoggerFactory loggerFactory) - { - _appHost = applicationHost; - _logger = logger; - _config = config; - _defaultRedirectPath = configuration[DefaultRedirectKey]; - _baseUrlPrefix = _config.Configuration.BaseUrl; - _networkManager = networkManager; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - ServiceController = serviceController; - _hostEnvironment = hostEnvironment; - _loggerFactory = loggerFactory; - - _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); - - Instance = this; - ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); - GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - } - - public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - - public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; } - - public static HttpListenerHost Instance { get; protected set; } - - public string[] UrlPrefixes { get; private set; } - - public string GlobalResponse { get; set; } - - public ServiceController ServiceController { get; } - - public object CreateInstance(Type type) - { - return _appHost.CreateInstance(type); - } - - private static string NormalizeUrlPath(string path) - { - if (path.Length > 0 && path[0] == '/') - { - // If the path begins with a leading slash, just return it as-is - return path; - } - else - { - // If the path does not begin with a leading slash, append one for consistency - return "/" + path; - } - } - - /// <summary> - /// Applies the request filters. Returns whether or not the request has been handled - /// and no more processing should be done. - /// </summary> - /// <returns></returns> - public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto) - { - // Exec all RequestFilter attributes with Priority < 0 - var attributes = GetRequestFilterAttributes(requestDto.GetType()); - - int count = attributes.Count; - int i = 0; - for (; i < count && attributes[i].Priority < 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - - // Exec remaining RequestFilter attributes with Priority >= 0 - for (; i < count && attributes[i].Priority >= 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - } - - public Type GetServiceTypeByRequest(Type requestType) - { - _serviceOperationsMap.TryGetValue(requestType, out var serviceType); - return serviceType; - } - - public void AddServiceInfo(Type serviceType, Type requestType) - { - _serviceOperationsMap[requestType] = serviceType; - } - - private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType) - { - var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList(); - - var serviceType = GetServiceTypeByRequest(requestDtoType); - if (serviceType != null) - { - attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>()); - } - - attributes.Sort((x, y) => x.Priority - y.Priority); - - return attributes; - } - - private static Exception GetActualException(Exception ex) - { - if (ex is AggregateException agg) - { - var inner = agg.InnerException; - if (inner != null) - { - return GetActualException(inner); - } - else - { - var inners = agg.InnerExceptions; - if (inners.Count > 0) - { - return GetActualException(inners[0]); - } - } - } - - return ex; - } - - private int GetStatusCode(Exception ex) - { - switch (ex) - { - case ArgumentException _: return 400; - case AuthenticationException _: return 401; - case SecurityException _: return 403; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return 404; - case MethodNotAllowedException _: return 405; - default: return 500; - } - } - - private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) - { - if (ignoreStackTrace) - { - _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); - } - else - { - _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); - } - - var httpRes = httpReq.Response; - - if (httpRes.HasStarted) - { - return; - } - - httpRes.StatusCode = statusCode; - - var errContent = _hostEnvironment.IsDevelopment() - ? (NormalizeExceptionMessage(ex) ?? string.Empty) - : "Error processing request."; - httpRes.ContentType = "text/plain"; - httpRes.ContentLength = errContent.Length; - await httpRes.WriteAsync(errContent).ConfigureAwait(false); - } - - private string NormalizeExceptionMessage(Exception ex) - { - // Do not expose the exception message for AuthenticationException - if (ex is AuthenticationException) - { - return null; - } - - // Strip any information we don't want to reveal - return ex.Message - ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); - } - - public static string RemoveQueryStringByKey(string url, string key) - { - var uri = new Uri(url); - - // this gets all the query string key value pairs as a collection - var newQueryString = QueryHelpers.ParseQuery(uri.Query); - - var originalCount = newQueryString.Count; - - if (originalCount == 0) - { - return url; - } - - // this removes the key if exists - newQueryString.Remove(key); - - if (originalCount == newQueryString.Count) - { - return url; - } - - // this gets the page path from root without QueryString - string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; - - return newQueryString.Count > 0 - ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) - : pagePathWithoutQueryString; - } - - private static string GetUrlToLog(string url) - { - url = RemoveQueryStringByKey(url, "api_key"); - - return url; - } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } - - private bool ValidateHost(string host) - { - var hosts = _config - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); - - if (hosts.Count == 0) - { - return true; - } - - host ??= string.Empty; - - if (_networkManager.IsInPrivateAddressSpace(host)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); - } - - return true; - } - - private bool ValidateRequest(string remoteIp, bool isLocal) - { - if (isLocal) - { - return true; - } - - if (_config.Configuration.EnableRemoteAccess) - { - var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - - if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) - { - if (_config.Configuration.IsRemoteIPFilterBlacklist) - { - return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - else - { - return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - } - } - else - { - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// <summary> - /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. - /// </summary> - /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns> - private bool ValidateSsl(string remoteIp, string urlString) - { - if (_config.Configuration.RequireHttps - && _appHost.ListenWithHttps - && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) - { - // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// <inheritdoc /> - public Task RequestHandler(HttpContext context) - { - if (context.WebSockets.IsWebSocketRequest) - { - return WebSocketRequestHandler(context); - } - - var request = context.Request; - var response = context.Response; - var localPath = context.Request.Path.ToString(); - - var req = new WebSocketSharpRequest(request, response, request.Path); - return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); - } - - /// <summary> - /// Overridable method that can be used to implement a custom handler. - /// </summary> - private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) - { - var stopWatch = new Stopwatch(); - stopWatch.Start(); - var httpRes = httpReq.Response; - string urlToLog = GetUrlToLog(urlString); - string remoteIp = httpReq.RemoteIp; - - try - { - if (_disposed) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateHost(host)) - { - httpRes.StatusCode = 400; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateRequest(remoteIp, httpReq.IsLocal)) - { - httpRes.StatusCode = 403; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateSsl(httpReq.RemoteIp, urlString)) - { - RedirectToSecureUrl(httpReq, httpRes, urlString); - return; - } - - if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - httpRes.Headers.Add(key, value); - } - - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing a URL at {0}", localPath); - httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); - return; - } - - if (!string.IsNullOrEmpty(GlobalResponse)) - { - // We don't want the address pings in ApplicationHost to fail - if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); - return; - } - } - - var handler = GetServiceHandler(httpReq); - if (handler != null) - { - await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false); - } - else - { - throw new FileNotFoundException(); - } - } - catch (Exception requestEx) - { - try - { - var requestInnerEx = GetActualException(requestEx); - var statusCode = GetStatusCode(requestInnerEx); - - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - if (!httpRes.Headers.ContainsKey(key)) - { - httpRes.Headers.Add(key, value); - } - } - - bool ignoreStackTrace = - requestInnerEx is SocketException - || requestInnerEx is IOException - || requestInnerEx is OperationCanceledException - || requestInnerEx is SecurityException - || requestInnerEx is AuthenticationException - || requestInnerEx is FileNotFoundException; - - // Do not handle 500 server exceptions manually when in development mode. - // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. - // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, - // because it will log the stack trace when it handles the exception. - if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) - { - throw; - } - - await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); - } - catch (Exception handlerException) - { - var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException); - _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog); - - if (_hostEnvironment.IsDevelopment()) - { - throw aggregateEx; - } - } - } - finally - { - if (httpRes.StatusCode >= 500) - { - _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); - } - - stopWatch.Stop(); - var elapsed = stopWatch.Elapsed; - if (elapsed.TotalMilliseconds > 500) - { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); - } - } - } - - private async Task WebSocketRequestHandler(HttpContext context) - { - if (_disposed) - { - return; - } - - try - { - _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); - - WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - - using var connection = new WebSocketConnection( - _loggerFactory.CreateLogger<WebSocketConnection>(), - webSocket, - context.Connection.RemoteIpAddress, - context.Request.Query) - { - OnReceive = ProcessWebSocketMessageReceived - }; - - WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); - - await connection.ProcessAsync().ConfigureAwait(false); - _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); - } - catch (Exception ex) // Otherwise ASP.Net will ignore the exception - { - _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); - if (!context.Response.HasStarted) - { - context.Response.StatusCode = 500; - } - } - } - - /// <summary> - /// Get the default CORS headers. - /// </summary> - /// <param name="req"></param> - /// <returns></returns> - public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req) - { - var origin = req.Headers["Origin"]; - if (origin == StringValues.Empty) - { - origin = req.Headers["Host"]; - if (origin == StringValues.Empty) - { - origin = "*"; - } - } - - var headers = new Dictionary<string, string>(); - headers.Add("Access-Control-Allow-Origin", origin); - headers.Add("Access-Control-Allow-Credentials", "true"); - headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); - return headers; - } - - // Entry point for HttpListener - public ServiceHandler GetServiceHandler(IHttpRequest httpReq) - { - var pathInfo = httpReq.PathInfo; - - pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); - var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); - if (restPath != null) - { - return new ServiceHandler(restPath, contentType); - } - - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); - return null; - } - - private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url) - { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - var builder = new UriBuilder(uri) - { - Port = _config.Configuration.PublicHttpsPort, - Scheme = "https" - }; - url = builder.Uri.ToString(); - } - - httpRes.Redirect(url); - } - - /// <summary> - /// Adds the rest handlers. - /// </summary> - /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param> - /// <param name="listeners">The web socket listeners.</param> - /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param> - public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes) - { - _webSocketListeners = listeners.ToArray(); - UrlPrefixes = urlPrefixes.ToArray(); - - ServiceController.Init(this, serviceTypes); - - ResponseFilters = new Action<IRequest, HttpResponse, object>[] - { - new ResponseFilter(this, _logger).FilterResponse - }; - } - - public RouteAttribute[] GetRouteAttributes(Type requestType) - { - var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList(); - var clone = routes.ToList(); - - foreach (var route in clone) - { - routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - } - - return routes.ToArray(); - } - - public Func<string, object> GetParseFn(Type propertyType) - { - return _funcParseFn(propertyType); - } - - public void SerializeToJson(object o, Stream stream) - { - _jsonSerializer.SerializeToStream(o, stream); - } - - public void SerializeToXml(object o, Stream stream) - { - _xmlSerializer.SerializeToStream(o, stream); - } - - public Task<object> DeserializeXml(Type type, Stream stream) - { - return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream)); - } - - public Task<object> DeserializeJson(Type type, Stream stream) - { - return _jsonSerializer.DeserializeFromStreamAsync(stream, type); - } - - private string NormalizeEmbyRoutePath(string path) - { - _logger.LogDebug("Normalizing /emby route"); - return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path); - } - - private string NormalizeMediaBrowserRoutePath(string path) - { - _logger.LogDebug("Normalizing /mediabrowser route"); - return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path); - } - - private string NormalizeCustomRoutePath(string path) - { - _logger.LogDebug("Normalizing custom route {0}", path); - return _baseUrlPrefix + NormalizeUrlPath(path); - } - - /// <summary> - /// Processes the web socket message received. - /// </summary> - /// <param name="result">The result.</param> - private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - if (_disposed) - { - return Task.CompletedTask; - } - - IEnumerable<Task> GetTasks() - { - foreach (var x in _webSocketListeners) - { - yield return x.ProcessMessageAsync(result); - } - } - - return Task.WhenAll(GetTasks()); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs deleted file mode 100644 index 688216373..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ /dev/null @@ -1,721 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.Services; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IRequest = MediaBrowser.Model.Services.IRequest; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class HttpResultFactory. - /// </summary> - public class HttpResultFactory : IHttpResultFactory - { - // Last-Modified and If-Modified-Since must follow strict date format, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since - private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; - // We specifically use en-US culture because both day of week and month names require it - private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); - - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<HttpResultFactory> _logger; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - private readonly IStreamHelper _streamHelper; - - /// <summary> - /// Initializes a new instance of the <see cref="HttpResultFactory" /> class. - /// </summary> - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) - { - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _streamHelper = streamHelper; - _logger = loggerfactory.CreateLogger<HttpResultFactory>(); - } - - /// <summary> - /// Gets the result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="content">The content.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(null, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetRedirectResult(string url) - { - var responseHeaders = new Dictionary<string, string>(); - responseHeaders[HeaderNames.Location] = url; - - var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - var result = new StreamWriter(content, contentType); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - string compressionType = null; - bool isHeadRequest = false; - - if (requestContext != null) - { - compressionType = GetCompressionType(requestContext, content, contentType); - isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - } - - IHasHeaders result; - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = content.Length; - - if (isHeadRequest) - { - content = Array.Empty<byte>(); - } - - result = new StreamWriter(content, contentType, contentLength); - } - else - { - result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - IHasHeaders result; - - var bytes = Encoding.UTF8.GetBytes(content); - - var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType); - - var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = bytes.Length; - - if (isHeadRequest) - { - bytes = Array.Empty<byte>(); - } - - result = new StreamWriter(bytes, contentType, contentLength); - } - else - { - result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the optimized result. - /// </summary> - /// <typeparam name="T"></typeparam> - public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) - where T : class - { - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - responseHeaders[HeaderNames.Expires] = "0"; - - return ToOptimizedResultInternal(requestContext, result, responseHeaders); - } - - private string GetCompressionType(IRequest request, byte[] content, string responseContentType) - { - if (responseContentType == null) - { - return null; - } - - // Per apple docs, hls manifests must be compressed - if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && - responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1) - { - return null; - } - - if (content.Length < 1024) - { - return null; - } - - return GetCompressionType(request); - } - - private static string GetCompressionType(IRequest request) - { - var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - - if (!string.IsNullOrEmpty(acceptEncoding)) - { - // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) - // return "br"; - - if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) - { - return "deflate"; - } - - if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) - { - return "gzip"; - } - } - - return null; - } - - /// <summary> - /// Returns the optimized result for the IRequestContext. - /// Does not use or store results in any cache. - /// </summary> - /// <param name="request"></param> - /// <param name="dto"></param> - /// <returns></returns> - public object ToOptimizedResult<T>(IRequest request, T dto) - { - return ToOptimizedResultInternal(request, dto); - } - - private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null) - { - // TODO: @bond use Span and .Equals - var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant(); - - switch (contentType) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders); - - case "application/json": - case "text/json": - return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); - default: - break; - } - - var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); - - ms.Position = 0; - - if (isHeadRequest) - { - using (ms) - { - return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders); - } - } - - return GetHttpResult(request, ms, contentType, true, responseHeaders); - } - - private IHasHeaders GetCompressedResult( - byte[] content, - string requestedCompressionType, - IDictionary<string, string> responseHeaders, - bool isHeadRequest, - string contentType) - { - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - content = Compress(content, requestedCompressionType); - responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - - responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; - - var contentLength = content.Length; - - if (isHeadRequest) - { - var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - else - { - var result = new StreamWriter(content, contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - } - - private byte[] Compress(byte[] bytes, string compressionType) - { - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) - { - return Deflate(bytes); - } - - if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) - { - return GZip(bytes); - } - - throw new NotSupportedException(compressionType); - } - - private static byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) - { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) - { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(ms)) - { - return reader.ReadToEnd(); - } - } - } - } - - /// <summary> - /// Pres the process optimized result. - /// </summary> - private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options) - { - bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; - AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); - - if (!noCache) - { - if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader)) - { - _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); - return null; - } - - if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) - { - AddAgeHeader(responseHeaders, options.DateLastModified); - - var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - } - - return null; - } - - public Task<object> GetStaticFileResult(IRequest requestContext, - string path, - FileShare fileShare = FileShare.Read) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - return GetStaticFileResult(requestContext, new StaticFileResultOptions - { - Path = path, - FileShare = fileShare - }); - } - - public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) - { - var path = options.Path; - var fileShare = options.FileShare; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("Path can't be empty.", nameof(options)); - } - - if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) - { - throw new ArgumentException("FileShare must be either Read or ReadWrite"); - } - - if (string.IsNullOrEmpty(options.ContentType)) - { - options.ContentType = MimeTypes.GetMimeType(path); - } - - if (!options.DateLastModified.HasValue) - { - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); - } - - options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); - - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - return GetStaticResult(requestContext, options); - } - - /// <summary> - /// Gets the file stream. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="fileShare">The file share.</param> - /// <returns>Stream.</returns> - private Stream GetFileStream(string path, FileShare fileShare) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare); - } - - public Task<object> GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, - Func<Task<Stream>> factoryFn, - IDictionary<string, string> responseHeaders = null, - bool isHeadRequest = false) - { - return GetStaticResult(requestContext, new StaticResultOptions - { - CacheDuration = cacheDuration, - ContentFactory = factoryFn, - ContentType = contentType, - DateLastModified = lastDateModified, - IsHeadRequest = isHeadRequest, - ResponseHeaders = responseHeaders - }); - } - - public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options) - { - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - var contentType = options.ContentType; - if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) - { - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, options); - - if (result != null) - { - return result; - } - } - - // TODO: We don't really need the option value - var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase); - var factoryFn = options.ContentFactory; - var responseHeaders = options.ResponseHeaders; - AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); - AddAgeHeader(responseHeaders, options.DateLastModified); - - var rangeHeader = requestContext.Headers[HeaderNames.Range]; - - if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) - { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) - { - OnComplete = options.OnComplete, - OnError = options.OnError, - FileShare = options.FileShare - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - - var stream = await factoryFn().ConfigureAwait(false); - - var totalContentLength = options.ContentLength; - if (!totalContentLength.HasValue) - { - try - { - totalContentLength = stream.Length; - } - catch (NotSupportedException) - { - } - } - - if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) - { - var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest) - { - OnComplete = options.OnComplete - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - else - { - if (totalContentLength.HasValue) - { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture); - } - - if (isHeadRequest) - { - using (stream) - { - return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders); - } - } - - var hasHeaders = new StreamWriter(stream, contentType) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - } - - /// <summary> - /// Adds the caching responseHeaders. - /// </summary> - private void AddCachingHeaders( - IDictionary<string, string> responseHeaders, - TimeSpan? cacheDuration, - bool noCache, - DateTime? lastModifiedDate) - { - if (noCache) - { - responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; - responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; - return; - } - - if (cacheDuration.HasValue) - { - responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; - } - else - { - responseHeaders[HeaderNames.CacheControl] = "public"; - } - - if (lastModifiedDate.HasValue) - { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); - } - } - - /// <summary> - /// Adds the age header. - /// </summary> - /// <param name="responseHeaders">The responseHeaders.</param> - /// <param name="lastDateModified">The last date modified.</param> - private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified) - { - if (lastDateModified.HasValue) - { - responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - - /// <summary> - /// Determines whether [is not modified] [the specified if modified since]. - /// </summary> - /// <param name="ifModifiedSince">If modified since.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="dateModified">The date modified.</param> - /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns> - private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - var lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - if (cacheDuration.HasValue) - { - var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); - - if (DateTime.UtcNow < cacheExpirationDate) - { - return true; - } - } - - return false; - } - - - /// <summary> - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that. - /// </summary> - /// <param name="date">The date.</param> - /// <returns>DateTime.</returns> - private static DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// <summary> - /// Adds the response headers. - /// </summary> - /// <param name="hasHeaders">The has options.</param> - /// <param name="responseHeaders">The response headers.</param> - private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders) - { - foreach (var item in responseHeaders) - { - hasHeaders.Headers[item.Key] = item.Value; - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs deleted file mode 100644 index 980c2cd3a..000000000 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult - { - private const int BufferSize = 81920; - - private readonly Dictionary<string, string> _options = new Dictionary<string, string>(); - - private List<KeyValuePair<long, long?>> _requestedRanges; - - /// <summary> - /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class. - /// </summary> - /// <param name="rangeHeader">The range header.</param> - /// <param name="contentLength">The content length.</param> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> - public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - RangeHeader = rangeHeader; - SourceStream = source; - IsHeadRequest = isHeadRequest; - - ContentType = contentType; - Headers[HeaderNames.ContentType] = contentType; - Headers[HeaderNames.AcceptRanges] = "bytes"; - StatusCode = HttpStatusCode.PartialContent; - - SetRangeValues(contentLength); - } - - /// <summary> - /// Gets or sets the source stream. - /// </summary> - /// <value>The source stream.</value> - private Stream SourceStream { get; set; } - private string RangeHeader { get; set; } - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - private long RangeEnd { get; set; } - private long RangeLength { get; set; } - private long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - /// <summary> - /// Additional HTTP Headers - /// </summary> - /// <value>The headers.</value> - public IDictionary<string, string> Headers => _options; - - /// <summary> - /// Gets the requested ranges. - /// </summary> - /// <value>The requested ranges.</value> - protected List<KeyValuePair<long, long?>> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List<KeyValuePair<long, long?>>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], CultureInfo.InvariantCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], CultureInfo.InvariantCulture); - } - - _requestedRanges.Add(new KeyValuePair<long, long?>(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// <summary> - /// Sets the range values. - /// </summary> - private void SetRangeValues(long contentLength) - { - var requestedRange = RequestedRanges[0]; - - TotalContentLength = contentLength; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - - if (RangeStart > 0 && SourceStream.CanSeek) - { - SourceStream.Position = RangeStart; - } - } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - using (var source = SourceStream) - { - // If the requested range is "0-", we can optimize by just doing a stream copy - if (RangeEnd >= TotalContentLength - 1) - { - await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false); - } - else - { - await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - OnComplete?.Invoke(); - } - } - - private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = ArrayPool<byte>.Shared.Rent(BufferSize); - try - { - int bytesRead; - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToCopy = Math.Min(bytesRead, copyLength); - - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false); - - copyLength -= bytesToCopy; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(array); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs deleted file mode 100644 index a8cd2ac8f..000000000 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class ResponseFilter. - /// </summary> - public class ResponseFilter - { - private readonly IHttpServer _server; - private readonly ILogger _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="ResponseFilter"/> class. - /// </summary> - /// <param name="server">The HTTP server.</param> - /// <param name="logger">The logger.</param> - public ResponseFilter(IHttpServer server, ILogger logger) - { - _server = server; - _logger = logger; - } - - /// <summary> - /// Filters the response. - /// </summary> - /// <param name="req">The req.</param> - /// <param name="res">The res.</param> - /// <param name="dto">The dto.</param> - public void FilterResponse(IRequest req, HttpResponse res, object dto) - { - foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) - { - res.Headers.Add(key, value); - } - // Try to prevent compatibility view - res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " + - "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + - "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + - "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + - "X-Emby-Authorization"; - - if (dto is Exception exception) - { - _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl); - - if (!string.IsNullOrEmpty(exception.Message)) - { - var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal); - error = RemoveControlCharacters(error); - - res.Headers.Add("X-Application-Error-Code", error); - } - } - - if (dto is IHasHeaders hasHeaders) - { - if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) - { - hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; - } - - // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) - && !string.IsNullOrEmpty(contentLength)) - { - var length = long.Parse(contentLength, CultureInfo.InvariantCulture); - - if (length > 0) - { - res.ContentLength = length; - } - } - } - } - - /// <summary> - /// Removes the control characters. - /// </summary> - /// <param name="inString">The in string.</param> - /// <returns>System.String.</returns> - public static string RemoveControlCharacters(string inString) - { - if (inString == null) - { - return null; - } - else if (inString.Length == 0) - { - return inString; - } - - var newString = new StringBuilder(inString.Length); - - foreach (var ch in inString) - { - if (!char.IsControl(ch)) - { - newString.Append(ch); - } - } - - return newString.ToString(); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 76c1d9bac..7d53e886f 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,17 +1,7 @@ #pragma warning disable CS1591 -using System; -using System.Linq; -using Emby.Server.Implementations.SocketSharp; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security @@ -19,228 +9,27 @@ namespace Emby.Server.Implementations.HttpServer.Security public class AuthService : IAuthService { private readonly IAuthorizationContext _authorizationContext; - private readonly ISessionManager _sessionManager; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; public AuthService( - IAuthorizationContext authorizationContext, - IServerConfigurationManager config, - ISessionManager sessionManager, - INetworkManager networkManager) + IAuthorizationContext authorizationContext) { _authorizationContext = authorizationContext; - _config = config; - _sessionManager = sessionManager; - _networkManager = networkManager; - } - - public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) - { - ValidateUser(request, authAttributes); - } - - public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) - { - var req = new WebSocketSharpRequest(request, null, request.Path); - var user = ValidateUser(req, authAttributes); - return user; } public AuthorizationInfo Authenticate(HttpRequest request) { var auth = _authorizationContext.GetAuthorizationInfo(request); - if (auth?.User == null) + if (auth == null) { - return null; + throw new SecurityException("Unauthenticated request."); } - if (auth.User.HasPermission(PermissionKind.IsDisabled)) + if (auth.User?.HasPermission(PermissionKind.IsDisabled) ?? false) { throw new SecurityException("User account has been disabled."); } return auth; } - - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) - { - // This code is executed before the service - var auth = _authorizationContext.GetAuthorizationInfo(request); - - if (!IsExemptFromAuthenticationToken(authAttributes, request)) - { - ValidateSecurityToken(request, auth.Token); - } - - if (authAttributes.AllowLocalOnly && !request.IsLocal) - { - throw new SecurityException("Operation not found."); - } - - var user = auth.User; - - if (user == null && auth.UserId != Guid.Empty) - { - throw new AuthenticationException("User with Id " + auth.UserId + " not found"); - } - - if (user != null) - { - ValidateUserAccess(user, request, authAttributes); - } - - var info = GetTokenInfo(request); - - if (!IsExemptFromRoles(auth, authAttributes, request, info)) - { - var roles = authAttributes.GetRoles(); - - ValidateRoles(roles, user); - } - - if (!string.IsNullOrEmpty(auth.DeviceId) && - !string.IsNullOrEmpty(auth.Client) && - !string.IsNullOrEmpty(auth.Device)) - { - _sessionManager.LogSessionActivity( - auth.Client, - auth.Version, - auth.DeviceId, - auth.Device, - request.RemoteIp, - user); - } - - return user; - } - - private void ValidateUserAccess( - User user, - IRequest request, - IAuthenticationAttributes authAttributes) - { - if (user.HasPermission(PermissionKind.IsDisabled)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.IsAdministrator) - && !authAttributes.EscapeParentalControl - && !user.IsParentalScheduleAllowed()) - { - request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); - - throw new SecurityException("This user account is not allowed access at this time."); - } - } - - private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (authAttribtues.IgnoreLegacyAuth) - { - return true; - } - - return false; - } - - private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (string.IsNullOrEmpty(auth.Token)) - { - return true; - } - - if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty)) - { - return true; - } - - return false; - } - - private static void ValidateRoles(string[] roles, User user) - { - if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new SecurityException("User does not have admin access."); - } - } - - if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) - { - throw new SecurityException("User does not have delete access."); - } - } - - if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - throw new SecurityException("User does not have download access."); - } - } - } - - private static AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; - } - - private void ValidateSecurityToken(IRequest request, string token) - { - if (string.IsNullOrEmpty(token)) - { - throw new AuthenticationException("Access token is required."); - } - - var info = GetTokenInfo(request); - - if (info == null) - { - throw new AuthenticationException("Access token is invalid or expired."); - } - } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index fb93fae3e..de7e7bf3b 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -7,7 +7,6 @@ using System.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security _userManager = userManager; } - public AuthorizationInfo GetAuthorizationInfo(object requestContext) + public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext) { - return GetAuthorizationInfo((IRequest)requestContext); - } - - public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext) - { - if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached)) + if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { return (AuthorizationInfo)cached; } @@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private AuthorizationInfo GetAuthorization(IRequest httpReq) + private AuthorizationInfo GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); var (authInfo, originalAuthInfo) = - GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); + GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); if (originalAuthInfo != null) { - httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; + httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo; } - httpReq.Items["AuthorizationInfo"] = authInfo; + httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -117,81 +111,89 @@ namespace Emby.Server.Implementations.HttpServer.Security Token = token }; - AuthenticationInfo originalAuthenticationInfo = null; - if (!string.IsNullOrWhiteSpace(token)) + if (string.IsNullOrWhiteSpace(token)) { - var result = _authRepo.Get(new AuthenticationInfoQuery - { - AccessToken = token - }); + // Request doesn't contain a token. + return (null, null); + } - originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; + var result = _authRepo.Get(new AuthenticationInfoQuery + { + AccessToken = token + }); - if (originalAuthenticationInfo != null) - { - var updateToken = false; + var originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null; - // TODO: Remove these checks for IsNullOrWhiteSpace - if (string.IsNullOrWhiteSpace(authInfo.Client)) - { - authInfo.Client = originalAuthenticationInfo.AppName; - } + if (originalAuthenticationInfo != null) + { + var updateToken = false; - if (string.IsNullOrWhiteSpace(authInfo.DeviceId)) - { - authInfo.DeviceId = originalAuthenticationInfo.DeviceId; - } + // TODO: Remove these checks for IsNullOrWhiteSpace + if (string.IsNullOrWhiteSpace(authInfo.Client)) + { + authInfo.Client = originalAuthenticationInfo.AppName; + } - // 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.DeviceId)) + { + authInfo.DeviceId = originalAuthenticationInfo.DeviceId; + } - 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; - } - } + // 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.Version)) - { - authInfo.Version = originalAuthenticationInfo.AppVersion; - } - else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) + if (string.IsNullOrWhiteSpace(authInfo.Device)) + { + authInfo.Device = originalAuthenticationInfo.DeviceName; + } + else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) { - if (allowTokenInfoUpdate) - { - updateToken = true; - originalAuthenticationInfo.AppVersion = authInfo.Version; - } + updateToken = true; + originalAuthenticationInfo.DeviceName = authInfo.Device; } + } - if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) + if (string.IsNullOrWhiteSpace(authInfo.Version)) + { + authInfo.Version = originalAuthenticationInfo.AppVersion; + } + else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase)) + { + if (allowTokenInfoUpdate) { - originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow; updateToken = true; + originalAuthenticationInfo.AppVersion = authInfo.Version; } + } - if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) - { - authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); + if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3) + { + originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow; + updateToken = true; + } - if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) - { - originalAuthenticationInfo.UserName = authInfo.User.Username; - updateToken = true; - } - } + if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty)) + { + authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId); - if (updateToken) + if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase)) { - _authRepo.Update(originalAuthenticationInfo); + originalAuthenticationInfo.UserName = authInfo.User.Username; + updateToken = true; } + + authInfo.IsApiKey = true; + } + else + { + authInfo.IsApiKey = false; + } + + if (updateToken) + { + _authRepo.Update(originalAuthenticationInfo); } } @@ -203,13 +205,13 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq) + private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq) { - var auth = httpReq.Headers["X-Emby-Authorization"]; + var auth = httpReq.Request.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers[HeaderNames.Authorization]; + auth = httpReq.Request.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); @@ -273,7 +275,7 @@ namespace Emby.Server.Implementations.HttpServer.Security if (param.Length == 2) { var value = NormalizeValue(param[1].Trim(new[] { '"' })); - result.Add(param[0], value); + result[param[0]] = value; } } diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 03fcfa53d..86914dea2 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -2,11 +2,11 @@ using System; using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security { @@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public SessionInfo GetSession(IRequest requestContext) + public SessionInfo GetSession(HttpContext requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); - } - - private AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.GetNormalizedRemoteIp(), user); } public SessionInfo GetSession(object requestContext) { - return GetSession((IRequest)requestContext); + return GetSession((HttpContext)requestContext); } - public User GetUser(IRequest requestContext) + public User GetUser(HttpContext requestContext) { var session = GetSession(requestContext); @@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public User GetUser(object requestContext) { - return GetUser((IRequest)requestContext); + return GetUser((HttpContext)requestContext); } } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs deleted file mode 100644 index 00e3ab8fe..000000000 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class StreamWriter. - /// </summary> - public class StreamWriter : IAsyncStreamWriter, IHasHeaders - { - /// <summary> - /// The options. - /// </summary> - private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); - - /// <summary> - /// Initializes a new instance of the <see cref="StreamWriter" /> class. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - public StreamWriter(Stream source, string contentType) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceStream = source; - - Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture); - } - - Headers[HeaderNames.ContentType] = contentType; - } - - /// <summary> - /// Initializes a new instance of the <see cref="StreamWriter"/> class. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="contentLength">The content length.</param> - public StreamWriter(byte[] source, string contentType, int contentLength) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceBytes = source; - - Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentType] = contentType; - } - - /// <summary> - /// Gets or sets the source stream. - /// </summary> - /// <value>The source stream.</value> - private Stream SourceStream { get; set; } - - private byte[] SourceBytes { get; set; } - - /// <summary> - /// Gets the options. - /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Headers => _options; - - /// <summary> - /// Fires when complete. - /// </summary> - public Action OnComplete { get; set; } - - /// <summary> - /// Fires when an error occours. - /// </summary> - public Action OnError { get; set; } - - /// <inheritdoc /> - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - var bytes = SourceBytes; - - if (bytes != null) - { - await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); - } - else - { - using (var src = SourceStream) - { - await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false); - } - } - } - catch - { - OnError?.Invoke(); - - throw; - } - finally - { - OnComplete?.Invoke(); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index d738047e0..fed2addf8 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using MediaBrowser.Common.Json; using MediaBrowser.Controller.Net; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -179,7 +180,7 @@ namespace Emby.Server.Implementations.HttpServer return; } - WebSocketMessage<object> stub; + WebSocketMessage<object>? stub; try { @@ -209,6 +210,12 @@ namespace Emby.Server.Implementations.HttpServer return; } + if (stub == null) + { + _logger.LogError("Error processing web socket message"); + return; + } + // Tell the PipeReader how much of the buffer we have consumed reader.AdvanceTo(buffer.End); @@ -221,7 +228,7 @@ namespace Emby.Server.Implementations.HttpServer Connection = this }; - if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) + if (info.MessageType == SessionMessageType.KeepAlive) { await SendKeepAliveResponse().ConfigureAwait(false); } @@ -238,7 +245,7 @@ namespace Emby.Server.Implementations.HttpServer new WebSocketMessage<string> { MessageId = Guid.NewGuid(), - MessageType = "KeepAlive" + MessageType = SessionMessageType.KeepAlive }, CancellationToken.None); } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs new file mode 100644 index 000000000..71ece80a7 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -0,0 +1,95 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.HttpServer +{ + public class WebSocketManager : IWebSocketManager + { + private readonly Lazy<IEnumerable<IWebSocketListener>> _webSocketListeners; + private readonly ILogger<WebSocketManager> _logger; + private readonly ILoggerFactory _loggerFactory; + + private bool _disposed = false; + + public WebSocketManager( + Lazy<IEnumerable<IWebSocketListener>> webSocketListeners, + ILogger<WebSocketManager> logger, + ILoggerFactory loggerFactory) + { + _webSocketListeners = webSocketListeners; + _logger = logger; + _loggerFactory = loggerFactory; + } + + public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; + + /// <inheritdoc /> + public async Task WebSocketRequestHandler(HttpContext context) + { + if (_disposed) + { + return; + } + + try + { + _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); + + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + + using var connection = new WebSocketConnection( + _loggerFactory.CreateLogger<WebSocketConnection>(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); + + await connection.ProcessAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } + catch (Exception ex) // Otherwise ASP.Net will ignore the exception + { + _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } + } + } + + /// <summary> + /// Processes the web socket message received. + /// </summary> + /// <param name="result">The result.</param> + private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + if (_disposed) + { + return Task.CompletedTask; + } + + IEnumerable<Task> GetTasks() + { + var listeners = _webSocketListeners.Value; + foreach (var x in listeners) + { + yield return x.ProcessMessageAsync(result); + } + } + + return Task.WhenAll(GetTasks()); + } + } +} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index fe74f1de7..7435e9d0b 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO continue; } - _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); + _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path); try { @@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } catch (Exception ex) { - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } } } @@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO } } + /// <inheritdoc /> public void Dispose() { _disposed = true; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index a32b03aaa..3353fae9d 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -6,12 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using Emby.Server.Implementations.Library; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.IO; -using Emby.Server.Implementations.Library; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.IO @@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO /// </summary> private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + private bool _disposed = false; + /// <summary> /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. /// </summary> @@ -87,7 +88,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); + _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path); } } } @@ -492,8 +493,6 @@ namespace Emby.Server.Implementations.IO } } - private bool _disposed = false; - /// <summary> /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// </summary> @@ -522,24 +521,4 @@ namespace Emby.Server.Implementations.IO _disposed = true; } } - - public class LibraryMonitorStartup : IServerEntryPoint - { - private readonly ILibraryMonitor _monitor; - - public LibraryMonitorStartup(ILibraryMonitor monitor) - { - _monitor = monitor; - } - - public Task RunAsync() - { - _monitor.Start(); - return Task.CompletedTask; - } - - public void Dispose() - { - } - } } diff --git a/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs new file mode 100644 index 000000000..c51cf0545 --- /dev/null +++ b/Emby.Server.Implementations/IO/LibraryMonitorStartup.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; + +namespace Emby.Server.Implementations.IO +{ + /// <summary> + /// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor. + /// </summary> + public sealed class LibraryMonitorStartup : IServerEntryPoint + { + private readonly ILibraryMonitor _monitor; + + /// <summary> + /// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class. + /// </summary> + /// <param name="monitor">The library monitor.</param> + public LibraryMonitorStartup(ILibraryMonitor monitor) + { + _monitor = monitor; + } + + /// <inheritdoc /> + public Task RunAsync() + { + _monitor.Start(); + return Task.CompletedTask; + } + + /// <inheritdoc /> + public void Dispose() + { + } + } +} diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index ab6483bf9..3cb025111 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO } } - public virtual void SetReadOnly(string path, bool isReadOnly) - { - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - return; - } - - var info = GetExtendedFileSystemInfo(path); - - if (info.Exists && info.IsReadOnly != isReadOnly) - { - if (isReadOnly) - { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); - } - else - { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); - File.SetAttributes(path, attributes); - } - } - } - public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { if (OperatingSystem.Id != OperatingSystemId.Windows) @@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } - public virtual void SetExecutable(string path) - { - if (OperatingSystem.Id == OperatingSystemId.Darwin) - { - RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); - } - } - private static void RunProcess(string path, string args, string workingDirectory) { using (var process = Process.Start(new ProcessStartInfo diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index 40b397edc..c16ebd61b 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - private const int StreamCopyToBufferSize = 81920; - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); @@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO } } - public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); - try - { - int totalBytesRead = 0; - - int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - } - - return totalBytesRead; - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); + byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize); try { int bytesRead; diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index e7e72c686..4bef59543 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -17,11 +17,6 @@ namespace Emby.Server.Implementations bool IsService { get; } /// <summary> - /// Gets the value of the --noautorunwebapp command line option. - /// </summary> - bool NoAutoRunWebApp { get; } - - /// <summary> /// Gets the value of the --package-name command line option. /// </summary> string PackageName { get; } diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs index bf57382ed..afa4ec7b1 100644 --- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs +++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs @@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Images // return _libraryManager.GetItemList(new InternalItemsQuery // { // ArtistIds = new[] { item.Id }, - // IncludeItemTypes = new[] { typeof(MusicAlbum).Name }, + // IncludeItemTypes = new[] { nameof(MusicAlbum) }, // OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, // Limit = 4, // Recursive = true, diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 57302b506..5f7e51858 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -133,9 +133,20 @@ namespace Emby.Server.Implementations.Images protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items) { + var useBackdrop = primaryItem is CollectionFolder; return items .Select(i => { + // Use Backdrop instead of Primary image for Library images. + if (useBackdrop) + { + var backdrop = i.GetImageInfo(ImageType.Backdrop, 0); + if (backdrop != null && backdrop.IsLocalFile) + { + return backdrop.Path; + } + } + var image = i.GetImageInfo(ImageType.Primary, 0); if (image != null && image.IsLocalFile) { @@ -190,7 +201,7 @@ namespace Emby.Server.Implementations.Images return null; } - ImageProcessor.CreateImageCollage(options); + ImageProcessor.CreateImageCollage(options, primaryItem.Name); return outputPath; } diff --git a/Emby.Server.Implementations/Images/GenreImageProvider.cs b/Emby.Server.Implementations/Images/GenreImageProvider.cs index 1cd4cd66b..381788231 100644 --- a/Emby.Server.Implementations/Images/GenreImageProvider.cs +++ b/Emby.Server.Implementations/Images/GenreImageProvider.cs @@ -42,7 +42,12 @@ namespace Emby.Server.Implementations.Images return _libraryManager.GetItemList(new InternalItemsQuery { Genres = new[] { item.Name }, - IncludeItemTypes = new[] { typeof(MusicAlbum).Name, typeof(MusicVideo).Name, typeof(Audio).Name }, + IncludeItemTypes = new[] + { + nameof(MusicAlbum), + nameof(MusicVideo), + nameof(Audio) + }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, Recursive = true, @@ -77,7 +82,7 @@ namespace Emby.Server.Implementations.Images return _libraryManager.GetItemList(new InternalItemsQuery { Genres = new[] { item.Name }, - IncludeItemTypes = new[] { typeof(Series).Name, typeof(Movie).Name }, + IncludeItemTypes = new[] { nameof(Series), nameof(Movie) }, OrderBy = new[] { (ItemSortBy.Random, SortOrder.Ascending) }, Limit = 4, Recursive = true, diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7b770d940..f16eda1ec 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -513,10 +513,11 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(type)); } - if (key.StartsWith(_configurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal)) + string programDataPath = _configurationManager.ApplicationPaths.ProgramDataPath; + if (key.StartsWith(programDataPath, StringComparison.Ordinal)) { // Try to normalize paths located underneath program-data in an attempt to make them more portable - key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) + key = key.Substring(programDataPath.Length) .TrimStart('/', '\\') .Replace('/', '\\'); } @@ -729,7 +730,7 @@ namespace Emby.Server.Implementations.Library Directory.CreateDirectory(rootFolderPath); var rootFolder = GetItemById(GetNewItemId(rootFolderPath, typeof(AggregateFolder))) as AggregateFolder ?? - ((Folder) ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) + ((Folder)ResolvePath(_fileSystem.GetDirectoryInfo(rootFolderPath))) .DeepCopy<Folder, AggregateFolder>(); // In case program data folder was moved @@ -771,7 +772,7 @@ namespace Emby.Server.Implementations.Library if (folder.ParentId != rootFolder.Id) { folder.ParentId = rootFolder.Id; - folder.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); } rootFolder.AddVirtualChild(folder); @@ -871,17 +872,17 @@ namespace Emby.Server.Implementations.Library public Guid GetStudioId(string name) { - return GetItemByNameId<Studio>(Studio.GetPath, name); + return GetItemByNameId<Studio>(Studio.GetPath(name)); } public Guid GetGenreId(string name) { - return GetItemByNameId<Genre>(Genre.GetPath, name); + return GetItemByNameId<Genre>(Genre.GetPath(name)); } public Guid GetMusicGenreId(string name) { - return GetItemByNameId<MusicGenre>(MusicGenre.GetPath, name); + return GetItemByNameId<MusicGenre>(MusicGenre.GetPath(name)); } /// <summary> @@ -943,7 +944,7 @@ namespace Emby.Server.Implementations.Library { var existing = GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(T).Name }, + IncludeItemTypes = new[] { nameof(MusicArtist) }, Name = name, DtoOptions = options }).Cast<MusicArtist>() @@ -957,13 +958,11 @@ namespace Emby.Server.Implementations.Library } } - var id = GetItemByNameId<T>(getPathFn, name); - + var path = getPathFn(name); + var id = GetItemByNameId<T>(path); var item = GetItemById(id) as T; - if (item == null) { - var path = getPathFn(name); item = new T { Name = name, @@ -979,10 +978,9 @@ namespace Emby.Server.Implementations.Library return item; } - private Guid GetItemByNameId<T>(Func<string, string> getPathFn, string name) + private Guid GetItemByNameId<T>(string path) where T : BaseItem, new() { - var path = getPathFn(name); var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds; return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); } @@ -1805,21 +1803,18 @@ namespace Emby.Server.Implementations.Library /// <param name="items">The items.</param> /// <param name="parent">The parent item.</param> /// <param name="cancellationToken">The cancellation token.</param> - public void CreateItems(IEnumerable<BaseItem> items, BaseItem parent, CancellationToken cancellationToken) + public void CreateItems(IReadOnlyList<BaseItem> items, BaseItem parent, CancellationToken cancellationToken) { - // Don't iterate multiple times - var itemsList = items.ToList(); - - _itemRepository.SaveItems(itemsList, cancellationToken); + _itemRepository.SaveItems(items, cancellationToken); - foreach (var item in itemsList) + foreach (var item in items) { RegisterItem(item); } if (ItemAdded != null) { - foreach (var item in itemsList) + foreach (var item in items) { // With the live tv guide this just creates too much noise if (item.SourceType != SourceType.Library) @@ -1868,7 +1863,8 @@ namespace Emby.Server.Implementations.Library return image.Path != null && !image.IsLocalFile; } - public void UpdateImages(BaseItem item, bool forceUpdate = false) + /// <inheritdoc /> + public async Task UpdateImagesAsync(BaseItem item, bool forceUpdate = false) { if (item == null) { @@ -1891,7 +1887,7 @@ namespace Emby.Server.Implementations.Library try { var index = item.GetImageIndex(img); - image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult(); + image = await ConvertImageToLocal(item, img, index).ConfigureAwait(false); } catch (ArgumentException) { @@ -1913,7 +1909,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Cannnot get image dimensions for {0}", image.Path); + _logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path); image.Width = 0; image.Height = 0; continue; @@ -1943,10 +1939,8 @@ namespace Emby.Server.Implementations.Library RegisterItem(item); } - /// <summary> - /// Updates the item. - /// </summary> - public void UpdateItems(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task UpdateItemsAsync(IReadOnlyList<BaseItem> items, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) { foreach (var item in items) { @@ -1957,7 +1951,7 @@ namespace Emby.Server.Implementations.Library item.DateLastSaved = DateTime.UtcNow; - UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); + await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } _itemRepository.SaveItems(items, cancellationToken); @@ -1991,17 +1985,9 @@ namespace Emby.Server.Implementations.Library } } - /// <summary> - /// Updates the item. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="parent">The parent item.</param> - /// <param name="updateReason">The update reason.</param> - /// <param name="cancellationToken">The cancellation token.</param> - public void UpdateItem(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) - { - UpdateItems(new[] { item }, parent, updateReason, cancellationToken); - } + /// <inheritdoc /> + public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) + => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); /// <summary> /// Reports the item removed. @@ -2233,7 +2219,7 @@ namespace Emby.Server.Implementations.Library if (refresh) { - item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None); + item.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal); } @@ -2420,7 +2406,7 @@ namespace Emby.Server.Implementations.Library if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase)) { item.ViewType = viewType; - item.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult(); } var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; @@ -2454,6 +2440,21 @@ namespace Emby.Server.Implementations.Library new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); } + public BaseItem GetParentItem(string parentId, Guid? userId) + { + if (!string.IsNullOrEmpty(parentId)) + { + return GetItemById(new Guid(parentId)); + } + + if (userId.HasValue && userId != Guid.Empty) + { + return GetUserRootFolder(); + } + + return RootFolder; + } + /// <inheritdoc /> public bool IsVideoFile(string path) { @@ -2902,7 +2903,7 @@ namespace Emby.Server.Implementations.Library await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); return item.GetImageInfo(image.Type, imageIndex); } @@ -2920,7 +2921,7 @@ namespace Emby.Server.Implementations.Library // Remove this image to prevent it from retrying over and over item.RemoveImage(image); - item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None); + await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); throw new InvalidOperationException(); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 67cf8bf5b..376a15570 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; - private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); private IMediaSourceProvider[] _providers; @@ -582,29 +583,20 @@ namespace Emby.Server.Implementations.Library mediaSource.InferTotalBitrate(); } - public async Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) + public Task<IDirectStreamProvider> GetDirectStreamProviderByUniqueId(string uniqueId, CancellationToken cancellationToken) { - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + var info = _openStreams.Values.FirstOrDefault(i => { - var info = _openStreams.Values.FirstOrDefault(i => + var liveStream = i as ILiveStream; + if (liveStream != null) { - var liveStream = i as ILiveStream; - if (liveStream != null) - { - return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); - } + return string.Equals(liveStream.UniqueId, uniqueId, StringComparison.OrdinalIgnoreCase); + } - return false; - }); + return false; + }); - return info as IDirectStreamProvider; - } - finally - { - _liveStreamSemaphore.Release(); - } + return Task.FromResult(info as IDirectStreamProvider); } public async Task<LiveStreamResponse> OpenLiveStream(LiveStreamRequest request, CancellationToken cancellationToken) @@ -793,29 +785,20 @@ namespace Emby.Server.Implementations.Library return new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider); } - private async Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) + private Task<ILiveStream> GetLiveStreamInfo(string id, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(id)) { throw new ArgumentNullException(nameof(id)); } - await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - try + if (_openStreams.TryGetValue(id, out ILiveStream info)) { - if (_openStreams.TryGetValue(id, out ILiveStream info)) - { - return info; - } - else - { - throw new ResourceNotFoundException(); - } + return Task.FromResult(info); } - finally + else { - _liveStreamSemaphore.Release(); + return Task.FromException<ILiveStream>(new ResourceNotFoundException()); } } @@ -844,7 +827,7 @@ namespace Emby.Server.Implementations.Library if (liveStream.ConsumerCount <= 0) { - _openStreams.Remove(id); + _openStreams.TryRemove(id, out _); _logger.LogInformation("Closing live stream {0}", id); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index 877fdec86..658c53f28 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -49,7 +49,7 @@ namespace Emby.Server.Implementations.Library var genres = item .GetRecursiveChildren(user, new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Audio).Name }, + IncludeItemTypes = new[] { nameof(Audio) }, DtoOptions = dtoOptions }) .Cast<Audio>() @@ -86,7 +86,7 @@ namespace Emby.Server.Implementations.Library { return _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Audio).Name }, + IncludeItemTypes = new[] { nameof(Audio) }, GenreIds = genreIds.ToArray(), diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index 03059e6d3..70be52411 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -32,7 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio /// <value>The priority.</value> public override ResolverPriority Priority => ResolverPriority.Fourth; - public MultiItemResolverResult ResolveMultiple(Folder parent, + public MultiItemResolverResult ResolveMultiple( + Folder parent, List<FileSystemMetadata> files, string collectionType, IDirectoryService directoryService) @@ -50,7 +51,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio return result; } - private MultiItemResolverResult ResolveMultipleInternal(Folder parent, + private MultiItemResolverResult ResolveMultipleInternal( + Folder parent, List<FileSystemMetadata> files, string collectionType, IDirectoryService directoryService) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 79b6dded3..18ceb5e76 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Emby.Naming.Audio; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -113,52 +116,48 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio IFileSystem fileSystem, ILibraryManager libraryManager) { + // check for audio files before digging down into directories + var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName)); + if (foundAudioFile) + { + // at least one audio file exists + return true; + } + + if (!allowSubfolders) + { + // not music since no audio file exists and we're not looking into subfolders + return false; + } + var discSubfolderCount = 0; - var notMultiDisc = false; var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); var parser = new AlbumParser(namingOptions); - foreach (var fileSystemInfo in list) + + var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory); + + var result = Parallel.ForEach(directories, (fileSystemInfo, state) => { - if (fileSystemInfo.IsDirectory) + var path = fileSystemInfo.FullName; + var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); + + if (hasMusic) { - if (allowSubfolders) + if (parser.IsMultiPart(path)) { - if (notMultiDisc) - { - continue; - } - - var path = fileSystemInfo.FullName; - var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager); - - if (hasMusic) - { - if (parser.IsMultiPart(path)) - { - logger.LogDebug("Found multi-disc folder: " + path); - discSubfolderCount++; - } - else - { - // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album - notMultiDisc = true; - } - } + logger.LogDebug("Found multi-disc folder: " + path); + Interlocked.Increment(ref discSubfolderCount); } - } - else - { - var fullName = fileSystemInfo.FullName; - - if (libraryManager.IsAudioFile(fullName)) + else { - return true; + // If there are folders underneath with music that are not multidisc, then this can't be a multi-disc album + state.Stop(); } } - } + }); - if (notMultiDisc) + if (!result.IsCompleted) { return false; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 5f5cd0e92..e9e688fa6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -94,7 +95,18 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager); // If we contain an album assume we are an artist folder - return args.FileSystemChildren.Where(i => i.IsDirectory).Any(i => albumResolver.IsMusicAlbum(i.FullName, directoryService)) ? new MusicArtist() : null; + var directories = args.FileSystemChildren.Where(i => i.IsDirectory); + + var result = Parallel.ForEach(directories, (fileSystemInfo, state) => + { + if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService)) + { + // stop once we see a music album + state.Stop(); + } + }); + + return !result.IsCompleted ? new MusicArtist() : null; } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 86a5d8b7d..59af7ce8a 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -50,7 +50,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var fileExtension = Path.GetExtension(f.FullName) ?? string.Empty; - return _validExtensions.Contains(fileExtension, + return _validExtensions.Contains( + fileExtension, StringComparer .OrdinalIgnoreCase); }).ToList(); diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 9a69bce0e..c850e3a08 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -87,61 +87,61 @@ namespace Emby.Server.Implementations.Library var excludeItemTypes = query.ExcludeItemTypes.ToList(); var includeItemTypes = (query.IncludeItemTypes ?? Array.Empty<string>()).ToList(); - excludeItemTypes.Add(typeof(Year).Name); - excludeItemTypes.Add(typeof(Folder).Name); + excludeItemTypes.Add(nameof(Year)); + excludeItemTypes.Add(nameof(Folder)); if (query.IncludeGenres && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Genre", StringComparer.OrdinalIgnoreCase))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, typeof(Genre).Name); - AddIfMissing(includeItemTypes, typeof(MusicGenre).Name); + AddIfMissing(includeItemTypes, nameof(Genre)); + AddIfMissing(includeItemTypes, nameof(MusicGenre)); } } else { - AddIfMissing(excludeItemTypes, typeof(Genre).Name); - AddIfMissing(excludeItemTypes, typeof(MusicGenre).Name); + AddIfMissing(excludeItemTypes, nameof(Genre)); + AddIfMissing(excludeItemTypes, nameof(MusicGenre)); } if (query.IncludePeople && (includeItemTypes.Count == 0 || includeItemTypes.Contains("People", StringComparer.OrdinalIgnoreCase) || includeItemTypes.Contains("Person", StringComparer.OrdinalIgnoreCase))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, typeof(Person).Name); + AddIfMissing(includeItemTypes, nameof(Person)); } } else { - AddIfMissing(excludeItemTypes, typeof(Person).Name); + AddIfMissing(excludeItemTypes, nameof(Person)); } if (query.IncludeStudios && (includeItemTypes.Count == 0 || includeItemTypes.Contains("Studio", StringComparer.OrdinalIgnoreCase))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, typeof(Studio).Name); + AddIfMissing(includeItemTypes, nameof(Studio)); } } else { - AddIfMissing(excludeItemTypes, typeof(Studio).Name); + AddIfMissing(excludeItemTypes, nameof(Studio)); } if (query.IncludeArtists && (includeItemTypes.Count == 0 || includeItemTypes.Contains("MusicArtist", StringComparer.OrdinalIgnoreCase))) { if (!query.IncludeMedia) { - AddIfMissing(includeItemTypes, typeof(MusicArtist).Name); + AddIfMissing(includeItemTypes, nameof(MusicArtist)); } } else { - AddIfMissing(excludeItemTypes, typeof(MusicArtist).Name); + AddIfMissing(excludeItemTypes, nameof(MusicArtist)); } - AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name); - AddIfMissing(excludeItemTypes, typeof(Folder).Name); + AddIfMissing(excludeItemTypes, nameof(CollectionFolder)); + AddIfMissing(excludeItemTypes, nameof(Folder)); var mediaTypes = query.MediaTypes.ToList(); if (includeItemTypes.Count > 0) diff --git a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs index d4c8c35e6..f9a3e2c64 100644 --- a/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs @@ -81,7 +81,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(MusicArtist).Name }, + IncludeItemTypes = new[] { nameof(MusicArtist) }, IsDeadArtist = true, IsLocked = false }).Cast<MusicArtist>().ToList(); diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 8275c873a..8739a9e1b 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Person).Name }, + IncludeItemTypes = new[] { nameof(Person) }, IsDeadPerson = true, IsLocked = false }); diff --git a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs index ca35adfff..9a8c5f39d 100644 --- a/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/StudiosValidator.cs @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Validators var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Studio).Name }, + IncludeItemTypes = new[] { nameof(Studio) }, IsDeadStudio = true, IsLocked = false }); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e13a3bb3..44560d1e2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -16,13 +16,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public class DirectRecorder : IRecorder { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IStreamHelper _streamHelper; - public DirectRecorder(ILogger logger, IHttpClient httpClient, IStreamHelper streamHelper) + public DirectRecorder(ILogger logger, IHttpClientFactory httpClientFactory, IStreamHelper streamHelper) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _streamHelper = streamHelper; } @@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false); + await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); } _logger.LogInformation("Recording completed to file {0}", targetFile); @@ -63,37 +63,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private async Task RecordFromMediaSource(MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { - var httpRequestOptions = new HttpRequestOptions - { - Url = mediaSource.Path, - BufferContent = false, - - // Some remote urls will expect a user agent to be supplied - UserAgent = "Emby/3.0", + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(mediaSource.Path, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethods.None - }; + _logger.LogInformation("Opened recording stream from tuner provider"); - using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) - { - _logger.LogInformation("Opened recording stream from tuner provider"); + Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); - Directory.CreateDirectory(Path.GetDirectoryName(targetFile)); + await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read); - using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - onStarted(); + onStarted(); - _logger.LogInformation("Copying recording stream to file {0}", targetFile); + _logger.LogInformation("Copying recording stream to file {0}", targetFile); - // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + // The media source if infinite so we need to handle stopping ourselves + var durationToken = new CancellationTokenSource(duration); + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); - } - } + await _streamHelper.CopyUntilCancelled( + await response.Content.ReadAsStreamAsync().ConfigureAwait(false), + output, + IODefaults.CopyToBufferSize, + cancellationToken).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 80e09f0a3..fcc2d1eeb 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -7,12 +7,14 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -29,7 +31,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; @@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IServerApplicationHost _appHost; private readonly ILogger<EmbyTV> _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; private readonly IJsonSerializer _jsonSerializer; @@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV IMediaSourceManager mediaSourceManager, ILogger<EmbyTV> logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerConfigurationManager config, ILiveTvManager liveTvManager, IFileSystem fileSystem, @@ -94,7 +95,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _appHost = appHost; _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _config = config; _fileSystem = fileSystem; _libraryManager = libraryManager; @@ -604,11 +605,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -808,11 +804,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings() - { - return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested); - } - public ActiveRecordingInfo GetActiveRecordingInfo(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -1015,16 +1006,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new Exception("Tuner not found."); } - private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) - { - var json = _jsonSerializer.SerializeToString(mediaSource); - mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - - mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - - return mediaSource; - } - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1654,10 +1635,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); } - return new DirectRecorder(_logger, _httpClient, _streamHelper); + return new DirectRecorder(_logger, _httpClientFactory, _streamHelper); } private void OnSuccessfulRecording(TimerInfo timer, string path) @@ -1809,7 +1790,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var program = string.IsNullOrWhiteSpace(timer.ProgramId) ? null : _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new[] { nameof(LiveTvProgram) }, Limit = 1, ExternalId = timer.ProgramId, DtoOptions = new DtoOptions(true) @@ -2170,7 +2151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var query = new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, Limit = 1, DtoOptions = new DtoOptions(true) { @@ -2389,7 +2370,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var query = new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, ExternalSeriesId = seriesTimer.SeriesId, DtoOptions = new DtoOptions(true) { @@ -2424,7 +2405,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV channel = _libraryManager.GetItemList( new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvChannel) }, ItemIds = new[] { parent.ChannelId }, DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; @@ -2483,7 +2464,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV channel = _libraryManager.GetItemList( new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvChannel).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvChannel) }, ItemIds = new[] { programInfo.ChannelId }, DtoOptions = new DtoOptions() }).FirstOrDefault() as LiveTvChannel; @@ -2548,7 +2529,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var seriesIds = _libraryManager.GetItemIds( new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Series).Name }, + IncludeItemTypes = new[] { nameof(Series) }, Name = program.Name }).ToArray(); @@ -2561,7 +2542,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var result = _libraryManager.GetItemIds(new InternalItemsQuery { - IncludeItemTypes = new[] { typeof(Episode).Name }, + IncludeItemTypes = new[] { nameof(Episode) }, ParentIndexNumber = program.SeasonNumber.Value, IndexNumber = program.EpisodeNumber.Value, AncestorIds = seriesIds, diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index d8ec107ec..3e5457dbd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,12 +8,9 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); + private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; - private readonly IJsonSerializer _json; - private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); - private readonly IServerConfigurationManager _config; public EncodedRecorder( ILogger logger, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, - IJsonSerializer json, - IServerConfigurationManager config) + IJsonSerializer json) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _config = config; } private static bool CopySubtitles => false; @@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } - private EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration<EncodingOptions>("encoding"); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV StartInfo = processStartInfo, EnableRaisingEvents = true }; - _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process); _process.Start(); @@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } protected string GetOutputSizeParam() - { - var filters = new List<string>(); - - filters.Add("yadif=0:-1:0"); - - var output = string.Empty; - - if (filters.Count > 0) - { - output += string.Format(" -vf \"{0}\"", string.Join(",", filters.ToArray())); - } - - return output; - } + => "-vf \"yadif=0:-1:0\""; private void Stop() { @@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <summary> /// Processes the exited. /// </summary> - private void OnFfMpegProcessExited(Process process, string inputFile) + private void OnFfMpegProcessExited(Process process) { using (process) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs index 69a9cb78a..a2ec2df37 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs @@ -5,7 +5,7 @@ using MediaBrowser.Controller.Plugins; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - public class EntryPoint : IServerEntryPoint + public sealed class EntryPoint : IServerEntryPoint { /// <inheritdoc /> public Task RunAsync() diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 285a59a24..dd479b7d1 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -5,8 +5,8 @@ using System.Collections.Concurrent; using System.Globalization; using System.Linq; using System.Threading; +using Jellyfin.Data.Events; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Events; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Serialization; using Microsoft.Extensions.Logging; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 77a7069eb..28aabc159 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -8,6 +8,8 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Mime; +using System.Text; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; @@ -24,23 +26,23 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + private readonly ILogger<SchedulesDirect> _logger; private readonly IJsonSerializer _jsonSerializer; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly IApplicationHost _appHost; - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect( ILogger<SchedulesDirect> logger, IJsonSerializer jsonSerializer, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IApplicationHost appHost) { _logger = logger; _jsonSerializer = jsonSerializer; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } @@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings while (start <= end) { - dates.Add(start.ToString("yyyy-MM-dd")); + dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); start = start.AddDays(1); } @@ -102,95 +104,78 @@ namespace Emby.Server.Implementations.LiveTv.Listings var requestString = _jsonSerializer.SerializeToString(requestList); _logger.LogDebug("Request string for schedules is: {RequestString}", requestString); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/schedules", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - RequestContent = requestString - }; - - httpOptions.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/schedules"); + options.Content = new StringContent(requestString, Encoding.UTF8, MediaTypeNames.Application.Json); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); + await using var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(responseStream).ConfigureAwait(false); + _logger.LogDebug("Found {ScheduleCount} programs on {ChannelID} ScheduleDirect", dailySchedules.Count, channelId); - using (var response = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - var dailySchedules = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Day>>(response.Content).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); - httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/programs", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; + var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct(); + programRequestOptions.Content = new StringContent("[\"" + string.Join("\", \"", programsID) + "\"]", Encoding.UTF8, MediaTypeNames.Application.Json); - httpOptions.RequestHeaders["token"] = token; + using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); + await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponseStream).ConfigureAwait(false); + var programDict = programDetails.ToDictionary(p => p.programID, y => y); - var programsID = dailySchedules.SelectMany(d => d.programs.Select(s => s.programID)).Distinct(); - httpOptions.RequestContent = "[\"" + string.Join("\", \"", programsID) + "\"]"; + var programIdsWithImages = + programDetails.Where(p => p.hasImageArtwork).Select(p => p.programID) + .ToList(); - using (var innerResponse = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - var programDetails = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ProgramDetails>>(innerResponse.Content).ConfigureAwait(false); - var programDict = programDetails.ToDictionary(p => p.programID, y => y); + var images = await GetImageForPrograms(info, programIdsWithImages, cancellationToken).ConfigureAwait(false); - 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)) + { + // _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); - var programsInfo = new List<ProgramInfo>(); - foreach (ScheduleDirect.Program schedule in dailySchedules.SelectMany(d => d.programs)) + if (images != null) + { + var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); + if (imageIndex > -1) { - // _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); + var programEntry = programDict[schedule.programID]; - if (images != null) - { - var imageIndex = images.FindIndex(i => i.programID == schedule.programID.Substring(0, 10)); - if (imageIndex > -1) - { - 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)); - - const double DesiredAspect = 2.0 / 3; + 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)); - programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? - GetProgramImage(ApiUrl, allImages, true, DesiredAspect); + const double DesiredAspect = 2.0 / 3; - const double WideAspect = 16.0 / 9; + programEntry.primaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ?? + GetProgramImage(ApiUrl, allImages, true, DesiredAspect); - programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); + const double WideAspect = 16.0 / 9; - // Don't supply the same image twice - if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) - { - programEntry.thumbImage = null; - } + programEntry.thumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect); - programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - - // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? - // GetProgramImage(ApiUrl, data, "Banner-LOT", false); - } + // Don't supply the same image twice + if (string.Equals(programEntry.primaryImage, programEntry.thumbImage, StringComparison.Ordinal)) + { + programEntry.thumbImage = null; } - programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); - } + programEntry.backdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect); - return programsInfo; + // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-L1", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LO", false) ?? + // GetProgramImage(ApiUrl, data, "Banner-LOT", false); + } } + + programsInfo.Add(GetProgram(channelId, schedule, programDict[schedule.programID])); } + + return programsInfo; } private static int GetSizeOrder(ScheduleDirect.ImageData image) @@ -367,13 +352,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate); + info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } 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; } @@ -482,22 +468,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings imageIdString = imageIdString.TrimEnd(',') + "]"; - var httpOptions = new HttpRequestOptions() + using var message = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/metadata/programs") { - Url = ApiUrl + "/metadata/programs", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - RequestContent = imageIdString, - LogErrorResponseBody = true, + Content = new StringContent(imageIdString, Encoding.UTF8, MediaTypeNames.Application.Json) }; try { - using (var innerResponse2 = await Post(httpOptions, true, info).ConfigureAwait(false)) - { - return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>( - innerResponse2.Content).ConfigureAwait(false); - } + using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); + await using var response = await innerResponse2.Content.ReadAsStreamAsync().ConfigureAwait(false); + return await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.ShowImages>>( + response).ConfigureAwait(false); } catch (Exception ex) { @@ -518,41 +499,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings return lineups; } - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/headends?country=" + country + "&postalcode=" + location, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/headends?country=" + country + "&postalcode=" + location); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, info).ConfigureAwait(false)) - using (Stream responce = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(responce).ConfigureAwait(false); + using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); + await using var response = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); - if (root != null) + var root = await _jsonSerializer.DeserializeFromStreamAsync<List<ScheduleDirect.Headends>>(response).ConfigureAwait(false); + + if (root != null) + { + foreach (ScheduleDirect.Headends headend in root) { - foreach (ScheduleDirect.Headends headend in root) + foreach (ScheduleDirect.Lineup lineup in headend.lineups) { - foreach (ScheduleDirect.Lineup lineup in headend.lineups) + lineups.Add(new NameIdPair { - 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.Substring(18) + }); } } - else - { - _logger.LogInformation("No lineups available"); - } + } + else + { + _logger.LogInformation("No lineups available"); } } catch (Exception ex) @@ -587,7 +560,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken = null; + NameValuePair savedToken; if (!_tokens.TryGetValue(username, out savedToken)) { savedToken = new NameValuePair(); @@ -633,16 +606,16 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private async Task<HttpResponseInfo> Post(HttpRequestOptions options, + private async Task<HttpResponseMessage> Send( + HttpRequestMessage options, bool enableRetry, - ListingsProviderInfo providerInfo) + ListingsProviderInfo providerInfo, + CancellationToken cancellationToken, + HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethods.Deflate; - try { - return await _httpClient.Post(options).ConfigureAwait(false); + return await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, completionOption, cancellationToken).ConfigureAwait(false); } catch (HttpException ex) { @@ -659,65 +632,28 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); - return await Post(options, false, providerInfo).ConfigureAwait(false); + options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); + return await Send(options, false, providerInfo, cancellationToken).ConfigureAwait(false); } - private async Task<HttpResponseInfo> Get(HttpRequestOptions options, - bool enableRetry, - ListingsProviderInfo providerInfo) - { - // Schedules direct requires that the client support compression and will return a 400 response without it - options.DecompressionMethod = CompressionMethods.Deflate; - - try - { - return await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); - } - catch (HttpException ex) - { - _tokens.Clear(); - - if (!ex.StatusCode.HasValue || (int)ex.StatusCode.Value >= 500) - { - enableRetry = false; - } - - if (!enableRetry) - { - throw; - } - } - - options.RequestHeaders["token"] = await GetToken(providerInfo, options.CancellationToken).ConfigureAwait(false); - return await Get(options, false, providerInfo).ConfigureAwait(false); - } - - private async Task<string> GetTokenInternal(string username, string password, + private async Task<string> GetTokenInternal( + string username, + string password, CancellationToken cancellationToken) { - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/token", - UserAgent = UserAgent, - RequestContent = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - // _logger.LogInformation("Obtaining token from Schedules Direct from addres: " + httpOptions.Url + " with body " + - // httpOptions.RequestContent); + using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token"); + options.Content = new StringContent("{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}", Encoding.UTF8, MediaTypeNames.Application.Json); - using (var response = await Post(httpOptions, false, null).ConfigureAwait(false)) + using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(stream).ConfigureAwait(false); + if (root.message == "OK") { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Token>(response.Content).ConfigureAwait(false); - if (root.message == "OK") - { - _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); - return root.token; - } - - throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); + _logger.LogInformation("Authenticated with Schedules Direct token: " + root.token); + return root.token; } + + throw new Exception("Could not authenticate with Schedules Direct Error: " + root.message); } private async Task AddLineupToAccount(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -736,20 +672,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Adding new LineUp "); - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + info.ListingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - BufferContent = false - }; - - httpOptions.RequestHeaders["token"] = token; - - using (await _httpClient.SendAsync(httpOptions, HttpMethod.Put).ConfigureAwait(false)) - { - } + using var options = new HttpRequestMessage(HttpMethod.Put, ApiUrl + "/lineups/" + info.ListingsId); + options.Headers.TryAddWithoutValidation("token", token); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); } private async Task<bool> HasLineup(ListingsProviderInfo info, CancellationToken cancellationToken) @@ -768,25 +693,17 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Headends on account "); - var options = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups", - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true - }; - - options.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups"); + options.Headers.TryAddWithoutValidation("token", token); try { - using (var httpResponse = await Get(options, false, null).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(response).ConfigureAwait(false); + using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); + await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var response = httpResponse.Content; + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Lineups>(stream).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 (HttpException ex) { @@ -851,55 +768,43 @@ namespace Emby.Server.Implementations.LiveTv.Listings throw new Exception("token required"); } - var httpOptions = new HttpRequestOptions() - { - Url = ApiUrl + "/lineups/" + listingsId, - UserAgent = UserAgent, - CancellationToken = cancellationToken, - LogErrorResponseBody = true, - }; - - httpOptions.RequestHeaders["token"] = token; + using var options = new HttpRequestMessage(HttpMethod.Get, ApiUrl + "/lineups/" + listingsId); + options.Headers.TryAddWithoutValidation("token", token); var list = new List<ChannelInfo>(); - using (var httpResponse = await Get(httpOptions, true, info).ConfigureAwait(false)) - using (var response = httpResponse.Content) - { - var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(response).ConfigureAwait(false); - _logger.LogInformation("Found {ChannelCount} channels on the lineup on ScheduleDirect", root.map.Count); - _logger.LogInformation("Mapping Stations to Channel"); + using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); + await using var stream = await httpResponse.Content.ReadAsStreamAsync().ConfigureAwait(false); + var root = await _jsonSerializer.DeserializeFromStreamAsync<ScheduleDirect.Channel>(stream).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 ?? Enumerable.Empty<ScheduleDirect.Station>(); + var allStations = root.stations ?? Enumerable.Empty<ScheduleDirect.Station>(); - foreach (ScheduleDirect.Map map in root.map) - { - var channelNumber = GetChannelNumber(map); - - var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); - if (station == null) - { - station = new ScheduleDirect.Station - { - stationID = map.stationID - }; - } + foreach (ScheduleDirect.Map map in root.map) + { + var channelNumber = GetChannelNumber(map); - var channelInfo = new ChannelInfo - { - Id = station.stationID, - CallSign = station.callsign, - Number = channelNumber, - Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name - }; + var station = allStations.FirstOrDefault(item => string.Equals(item.stationID, map.stationID, StringComparison.OrdinalIgnoreCase)); + if (station == null) + { + station = new ScheduleDirect.Station { stationID = map.stationID }; + } - if (station.logo != null) - { - channelInfo.ImageUrl = station.logo.URL; - } + var channelInfo = new ChannelInfo + { + Id = station.stationID, + CallSign = station.callsign, + Number = channelNumber, + Name = string.IsNullOrWhiteSpace(station.name) ? channelNumber : station.name + }; - list.Add(channelInfo); + if (station.logo != null) + { + channelInfo.ImageUrl = station.logo.URL; } + + list.Add(channelInfo); } return list; @@ -929,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private static string NormalizeName(string value) { - return value.Replace(" ", string.Empty).Replace("-", string.Empty); + return value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("-", string.Empty, StringComparison.Ordinal); } public class ScheduleDirect @@ -969,7 +874,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List<Lineup> lineups { get; set; } } - public class Headends { public string headend { get; set; } @@ -981,8 +885,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List<Lineup> lineups { get; set; } } - - public class Map { public string stationID { get; set; } @@ -1066,9 +968,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public List<string> date { get; set; } } - - - public class Rating { public string body { get; set; } @@ -1112,8 +1011,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings public string isPremiereOrFinale { get; set; } } - - public class MetadataSchedule { public string modified { get; set; } diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs index 0a93c4674..2d6f453bd 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs @@ -25,20 +25,20 @@ namespace Emby.Server.Implementations.LiveTv.Listings public class XmlTvListingsProvider : IListingsProvider { private readonly IServerConfigurationManager _config; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger<XmlTvListingsProvider> _logger; private readonly IFileSystem _fileSystem; private readonly IZipClient _zipClient; public XmlTvListingsProvider( IServerConfigurationManager config, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger<XmlTvListingsProvider> logger, IFileSystem fileSystem, IZipClient zipClient) { _config = config; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _logger = logger; _fileSystem = fileSystem; _zipClient = zipClient; @@ -78,28 +78,11 @@ namespace Emby.Server.Implementations.LiveTv.Listings Directory.CreateDirectory(Path.GetDirectoryName(cacheFile)); - using (var res = await _httpClient.SendAsync( - new HttpRequestOptions - { - CancellationToken = cancellationToken, - Url = path, - DecompressionMethod = CompressionMethods.Gzip, - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = res.Content) - using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(path, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using (var fileStream = new FileStream(cacheFile, FileMode.CreateNew)) { - if (res.ContentHeaders.ContentEncoding.Contains("gzip")) - { - using (var gzStream = new GZipStream(stream, CompressionMode.Decompress)) - { - await gzStream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - } - else - { - await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } + await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } return UnzipIfNeeded(path, cacheFile); @@ -237,7 +220,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings && !programInfo.IsRepeat && (programInfo.EpisodeNumber ?? 0) == 0) { - programInfo.ShowId = programInfo.ShowId + programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); + programInfo.ShowId += programInfo.StartDate.Ticks.ToString(CultureInfo.InvariantCulture); } } else @@ -246,7 +229,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings } // Construct an id from the channel and start date - programInfo.Id = string.Format("{0}_{1:O}", program.ChannelId, program.StartDate); + programInfo.Id = string.Format(CultureInfo.InvariantCulture, "{0}_{1:O}", program.ChannelId, program.StartDate); if (programInfo.IsMovie) { @@ -296,7 +279,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings Name = c.DisplayName, ImageUrl = c.Icon != null && !string.IsNullOrEmpty(c.Icon.Source) ? c.Icon.Source : null, Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number - }).ToList(); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 49ad73af3..6af49dd45 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.LiveTv { var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(Series).Name }, + IncludeItemTypes = new string[] { nameof(Series) }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, @@ -253,7 +253,7 @@ namespace Emby.Server.Implementations.LiveTv { var librarySeries = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(Series).Name }, + IncludeItemTypes = new string[] { nameof(Series) }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Thumb }, @@ -296,7 +296,7 @@ namespace Emby.Server.Implementations.LiveTv var program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(Series).Name }, + IncludeItemTypes = new string[] { nameof(Series) }, Name = seriesName, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, @@ -307,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv { program = _libraryManager.GetItemList(new InternalItemsQuery { - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, ExternalSeriesId = programSeriesId, Limit = 1, ImageTypes = new ImageType[] { ImageType.Primary }, diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 1b075d86a..9c7d624ee 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Emby.Server.Implementations.Library; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; @@ -24,7 +25,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; @@ -41,6 +41,7 @@ namespace Emby.Server.Implementations.LiveTv /// </summary> public class LiveTvManager : ILiveTvManager, IDisposable { + private const int MaxGuideDays = 14; private const string ExternalServiceTag = "ExternalServiceId"; private const string EtagKey = "ProgramEtag"; @@ -186,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv IsKids = query.IsKids, IsSports = query.IsSports, IsSeries = query.IsSeries, - IncludeItemTypes = new[] { typeof(LiveTvChannel).Name }, + IncludeItemTypes = new[] { nameof(LiveTvChannel) }, TopParentIds = new[] { topFolder.Id }, IsFavorite = query.IsFavorite, IsLiked = query.IsLiked, @@ -421,7 +422,7 @@ namespace Emby.Server.Implementations.LiveTv } } - private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) + private async Task<LiveTvChannel> GetChannelAsync(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken) { var parentFolderId = parentFolder.Id; var isNew = false; @@ -511,7 +512,7 @@ namespace Emby.Server.Implementations.LiveTv } else if (forceUpdate) { - _libraryManager.UpdateItem(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemAsync(item, parentFolder, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); } return item; @@ -560,7 +561,7 @@ namespace Emby.Server.Implementations.LiveTv item.Audio = info.Audio; item.ChannelId = channel.Id; - item.CommunityRating = item.CommunityRating ?? info.CommunityRating; + item.CommunityRating ??= info.CommunityRating; if ((item.CommunityRating ?? 0).Equals(0)) { item.CommunityRating = null; @@ -645,8 +646,8 @@ namespace Emby.Server.Implementations.LiveTv item.IsSeries = isSeries; item.Name = info.Name; - item.OfficialRating = item.OfficialRating ?? info.OfficialRating; - item.Overview = item.Overview ?? info.Overview; + item.OfficialRating ??= info.OfficialRating; + item.Overview ??= info.Overview; item.RunTimeTicks = (info.EndDate - info.StartDate).Ticks; item.ProviderIds = info.ProviderIds; @@ -683,19 +684,23 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ImagePath)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImagePath, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImagePath, + Type = ImageType.Primary + }, + 0); } else if (!string.IsNullOrWhiteSpace(info.ImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ImageUrl, - Type = ImageType.Primary - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ImageUrl, + Type = ImageType.Primary + }, + 0); } } @@ -703,11 +708,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.ThumbImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.ThumbImageUrl, - Type = ImageType.Thumb - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.ThumbImageUrl, + Type = ImageType.Thumb + }, + 0); } } @@ -715,11 +722,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.LogoImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.LogoImageUrl, - Type = ImageType.Logo - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.LogoImageUrl, + Type = ImageType.Logo + }, + 0); } } @@ -727,11 +736,13 @@ namespace Emby.Server.Implementations.LiveTv { if (!string.IsNullOrWhiteSpace(info.BackdropImageUrl)) { - item.SetImage(new ItemImageInfo - { - Path = info.BackdropImageUrl, - Type = ImageType.Backdrop - }, 0); + item.SetImage( + new ItemImageInfo + { + Path = info.BackdropImageUrl, + Type = ImageType.Backdrop + }, + 0); } } @@ -786,7 +797,6 @@ namespace Emby.Server.Implementations.LiveTv if (query.OrderBy.Count == 0) { - // Unless something else was specified, order by start date to take advantage of a specialized index query.OrderBy = new[] { @@ -798,7 +808,7 @@ namespace Emby.Server.Implementations.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new[] { nameof(LiveTvProgram) }, MinEndDate = query.MinEndDate, MinStartDate = query.MinStartDate, MaxEndDate = query.MaxEndDate, @@ -824,7 +834,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrWhiteSpace(query.SeriesTimerId)) { - var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery { }, cancellationToken).ConfigureAwait(false); + var seriesTimers = await GetSeriesTimersInternal(new SeriesTimerQuery(), cancellationToken).ConfigureAwait(false); var seriesTimer = seriesTimers.Items.FirstOrDefault(i => string.Equals(_tvDtoService.GetInternalSeriesTimerId(i.Id).ToString("N", CultureInfo.InvariantCulture), query.SeriesTimerId, StringComparison.OrdinalIgnoreCase)); if (seriesTimer != null) { @@ -847,13 +857,11 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - var result = new QueryResult<BaseItemDto> + return new QueryResult<BaseItemDto> { Items = returnArray, TotalRecordCount = queryResult.TotalRecordCount }; - - return result; } public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) @@ -864,7 +872,7 @@ namespace Emby.Server.Implementations.LiveTv var internalQuery = new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new[] { nameof(LiveTvProgram) }, IsAiring = query.IsAiring, HasAired = query.HasAired, IsNews = query.IsNews, @@ -1081,8 +1089,8 @@ namespace Emby.Server.Implementations.LiveTv if (cleanDatabase) { - CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { typeof(LiveTvChannel).Name }, progress, cancellationToken); - CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { typeof(LiveTvProgram).Name }, progress, cancellationToken); + CleanDatabaseInternal(newChannelIdList.ToArray(), new[] { nameof(LiveTvChannel) }, progress, cancellationToken); + CleanDatabaseInternal(newProgramIdList.ToArray(), new[] { nameof(LiveTvProgram) }, progress, cancellationToken); } var coreService = _services.OfType<EmbyTV.EmbyTV>().FirstOrDefault(); @@ -1121,7 +1129,7 @@ namespace Emby.Server.Implementations.LiveTv try { - var item = GetChannel(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken); + var item = await GetChannelAsync(channelInfo.Item2, channelInfo.Item1, parentFolder, cancellationToken).ConfigureAwait(false); list.Add(item); } @@ -1138,7 +1146,7 @@ namespace Emby.Server.Implementations.LiveTv double percent = numComplete; percent /= allChannelsList.Count; - progress.Report(5 * percent + 10); + progress.Report((5 * percent) + 10); } progress.Report(15); @@ -1173,8 +1181,7 @@ namespace Emby.Server.Implementations.LiveTv var existingPrograms = _libraryManager.GetItemList(new InternalItemsQuery { - - IncludeItemTypes = new string[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new string[] { nameof(LiveTvProgram) }, ChannelIds = new Guid[] { currentChannel.Id }, DtoOptions = new DtoOptions(true) }).Cast<LiveTvProgram>().ToDictionary(i => i.Id); @@ -1214,7 +1221,11 @@ namespace Emby.Server.Implementations.LiveTv if (updatedPrograms.Count > 0) { - _libraryManager.UpdateItems(updatedPrograms, currentChannel, ItemUpdateType.MetadataImport, cancellationToken); + await _libraryManager.UpdateItemsAsync( + updatedPrograms, + currentChannel, + ItemUpdateType.MetadataImport, + cancellationToken).ConfigureAwait(false); } currentChannel.IsMovie = isMovie; @@ -1227,7 +1238,7 @@ namespace Emby.Server.Implementations.LiveTv currentChannel.AddTag("Kids"); } - currentChannel.UpdateToRepository(ItemUpdateType.MetadataImport, cancellationToken); + await currentChannel.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false); await currentChannel.RefreshMetadata( new MetadataRefreshOptions(new DirectoryService(_fileSystem)) { @@ -1298,8 +1309,6 @@ namespace Emby.Server.Implementations.LiveTv } } - private const int MaxGuideDays = 14; - private double GetGuideDays() { var config = GetConfiguration(); @@ -1337,11 +1346,11 @@ namespace Emby.Server.Implementations.LiveTv { if (query.IsMovie.Value) { - includeItemTypes.Add(typeof(Movie).Name); + includeItemTypes.Add(nameof(Movie)); } else { - excludeItemTypes.Add(typeof(Movie).Name); + excludeItemTypes.Add(nameof(Movie)); } } @@ -1349,11 +1358,11 @@ namespace Emby.Server.Implementations.LiveTv { if (query.IsSeries.Value) { - includeItemTypes.Add(typeof(Episode).Name); + includeItemTypes.Add(nameof(Episode)); } else { - excludeItemTypes.Add(typeof(Episode).Name); + excludeItemTypes.Add(nameof(Episode)); } } @@ -1712,7 +1721,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("Timer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "Timer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1731,7 +1740,7 @@ namespace Emby.Server.Implementations.LiveTv if (timer == null) { - throw new ResourceNotFoundException(string.Format("SeriesTimer with Id {0} not found", id)); + throw new ResourceNotFoundException(string.Format(CultureInfo.InvariantCulture, "SeriesTimer with Id {0} not found", id)); } var service = GetService(timer.ServiceName); @@ -1743,10 +1752,12 @@ namespace Emby.Server.Implementations.LiveTv public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) { - var results = await GetTimers(new TimerQuery - { - Id = id - }, cancellationToken).ConfigureAwait(false); + var results = await GetTimers( + new TimerQuery + { + Id = id + }, + cancellationToken).ConfigureAwait(false); return results.Items.FirstOrDefault(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); } @@ -1794,10 +1805,7 @@ namespace Emby.Server.Implementations.LiveTv } var returnArray = timers - .Select(i => - { - return i.Item1; - }) + .Select(i => i.Item1) .ToArray(); return new QueryResult<SeriesTimerInfo> @@ -1875,7 +1883,7 @@ namespace Emby.Server.Implementations.LiveTv var programs = options.AddCurrentProgram ? _libraryManager.GetItemList(new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(LiveTvProgram).Name }, + IncludeItemTypes = new[] { nameof(LiveTvProgram) }, ChannelIds = channelIds, MaxStartDate = now, MinEndDate = now, @@ -1968,7 +1976,7 @@ namespace Emby.Server.Implementations.LiveTv if (service == null) { - service = _services.First(); + service = _services[0]; } var info = await service.GetNewTimerDefaultsAsync(cancellationToken, programInfo).ConfigureAwait(false); @@ -1994,9 +2002,7 @@ namespace Emby.Server.Implementations.LiveTv { var info = await GetNewTimerDefaultsInternal(cancellationToken).ConfigureAwait(false); - var obj = _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); - - return obj; + return _tvDtoService.GetSeriesTimerInfoDto(info.Item1, info.Item2, null); } public async Task<SeriesTimerInfoDto> GetNewTimerDefaults(string programId, CancellationToken cancellationToken) @@ -2125,9 +2131,11 @@ namespace Emby.Server.Implementations.LiveTv public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } private bool _disposed = false; + /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> @@ -2447,8 +2455,7 @@ namespace Emby.Server.Implementations.LiveTv .SelectMany(i => i.Locations) .Distinct(StringComparer.OrdinalIgnoreCase) .Select(i => _libraryManager.FindByPath(i, true)) - .Where(i => i != null) - .Where(i => i.IsVisibleStandalone(user)) + .Where(i => i != null && i.IsVisibleStandalone(user)) .SelectMany(i => _libraryManager.GetCollectionFolders(i)) .GroupBy(x => x.Id) .Select(x => x.First()) diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index f1b61f7c7..582b64923 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -43,7 +43,7 @@ namespace Emby.Server.Implementations.LiveTv return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index c61189c0a..2f4c60117 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -31,17 +31,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly ISocketFactory _socketFactory; private readonly INetworkManager _networkManager; private readonly IStreamHelper _streamHelper; + private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); + public HdHomerunHost( IServerConfigurationManager config, ILogger<HdHomerunHost> logger, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, ISocketFactory socketFactory, INetworkManager networkManager, @@ -49,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun IMemoryCache memoryCache) : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _socketFactory = socketFactory; _networkManager = networkManager; @@ -69,15 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - var options = new HttpRequestOptions - { - Url = model.LineupURL, - CancellationToken = cancellationToken, - BufferContent = false - }; - - using var response = await _httpClient.SendAsync(options, HttpMethod.Get).ConfigureAwait(false); - await using var stream = response.Content; + using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false) ?? new List<Channels>(); @@ -114,7 +109,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun }).Cast<ChannelInfo>().ToList(); } - private readonly Dictionary<string, DiscoverResponse> _modelCache = new Dictionary<string, DiscoverResponse>(); private async Task<DiscoverResponse> GetModelInfo(TunerHostInfo info, bool throwAllExceptions, CancellationToken cancellationToken) { var cacheKey = info.Id; @@ -132,14 +126,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun try { - using var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false); - await using var stream = response.Content; + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/discover.json", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, cancellationToken: cancellationToken) .ConfigureAwait(false); @@ -157,10 +147,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) { - var defaultValue = "HDHR"; + const string DefaultValue = "HDHR"; var response = new DiscoverResponse { - ModelNumber = defaultValue + ModelNumber = DefaultValue }; if (!string.IsNullOrEmpty(cacheKey)) { @@ -182,46 +172,41 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - using (var response = await _httpClient.SendAsync(new HttpRequestOptions() - { - Url = string.Format("{0}/tuners.html", GetApiUrl(info)), - CancellationToken = cancellationToken, - BufferContent = false - }, HttpMethod.Get).ConfigureAwait(false)) - using (var stream = response.Content) - using (var sr = new StreamReader(stream, System.Text.Encoding.UTF8)) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); + var tuners = new List<LiveTvTunerInfo>(); + while (!sr.EndOfStream) { - var tuners = new List<LiveTvTunerInfo>(); - while (!sr.EndOfStream) + string line = StripXML(sr.ReadLine()); + if (line.Contains("Channel", StringComparison.Ordinal)) { - string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel", StringComparison.Ordinal)) + LiveTvTunerStatus status; + var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = line.Substring(0, index - 1); + var currentChannel = line.Substring(index + 7); + if (currentChannel != "none") { - LiveTvTunerStatus status; - var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = line.Substring(0, index - 1); - var currentChannel = line.Substring(index + 7); - if (currentChannel != "none") - { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } - - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; } - } - return tuners; + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } + + return tuners; } private static string StripXML(string source) @@ -578,6 +563,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo info, ChannelInfo channelInfo, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { + var tunerCount = info.TunerCount; + + if (tunerCount > 0) + { + var tunerHostId = info.Id; + var liveStreams = currentLiveStreams.Where(i => string.Equals(i.TunerHostId, tunerHostId, StringComparison.OrdinalIgnoreCase)); + + if (liveStreams.Count() >= tunerCount) + { + throw new LiveTvConflictException("HDHomeRun simultaneous stream limit has been reached."); + } + } + var profile = streamId.Split('_')[0]; Logger.LogInformation("GetChannelStream: channel id: {0}. stream id: {1} profile: {2}", channelInfo.Id, streamId, profile); @@ -631,7 +629,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun info, streamId, FileSystem, - _httpClient, + _httpClientFactory, Logger, Config, _appHost, @@ -730,7 +728,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // Need a way to set the Receive timeout on the socket otherwise this might never timeout? try { - await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken); + await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false); var receiveBuffer = new byte[8192]; while (!cancellationToken.IsCancellationRequested) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6730751d5..858c10030 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -131,6 +131,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun await taskCompletionSource.Task.ConfigureAwait(false); } + public string GetFilePath() + { + return TempFilePath; + } + private Task StartStreaming(UdpClient udpClient, HdHomerunManager hdHomerunManager, IPAddress remoteAddress, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 8fc29fb4a..4b170b2e4 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; @@ -26,7 +27,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; private readonly INetworkManager _networkManager; private readonly IMediaSourceManager _mediaSourceManager; @@ -37,14 +38,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts IMediaSourceManager mediaSourceManager, ILogger<M3UTunerHost> logger, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, IServerApplicationHost appHost, INetworkManager networkManager, IStreamHelper streamHelper, IMemoryCache memoryCache) : base(config, logger, fileSystem, memoryCache) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; _networkManager = networkManager; _mediaSourceManager = mediaSourceManager; @@ -64,7 +65,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var channelIdPrefix = GetFullChannelIdPrefix(info); - return await new M3uParser(Logger, _httpClient, _appHost).Parse(info.Url, channelIdPrefix, info.Id, cancellationToken).ConfigureAwait(false); + return await new M3uParser(Logger, _httpClientFactory, _appHost) + .Parse(info, channelIdPrefix, cancellationToken) + .ConfigureAwait(false); } public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) @@ -116,7 +119,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { - return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClient, Logger, Config, _appHost, _streamHelper); + return new SharedHttpStream(mediaSource, info, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper); } } @@ -125,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public async Task Validate(TunerHostInfo info) { - using (var stream = await new M3uParser(Logger, _httpClient, _appHost).GetListingsStream(info.Url, CancellationToken.None).ConfigureAwait(false)) + using (var stream = await new M3uParser(Logger, _httpClientFactory, _appHost).GetListingsStream(info, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index 875977219..c064e2fe6 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -12,6 +13,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts @@ -19,22 +21,22 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public class M3uParser { private readonly ILogger _logger; - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; - public M3uParser(ILogger logger, IHttpClient httpClient, IServerApplicationHost appHost) + public M3uParser(ILogger logger, IHttpClientFactory httpClientFactory, IServerApplicationHost appHost) { _logger = logger; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; } - public async Task<List<ChannelInfo>> Parse(string url, string channelIdPrefix, string tunerHostId, CancellationToken cancellationToken) + public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken) { // Read the file and display it line by line. - using (var reader = new StreamReader(await GetListingsStream(url, cancellationToken).ConfigureAwait(false))) + using (var reader = new StreamReader(await GetListingsStream(info, cancellationToken).ConfigureAwait(false))) { - return GetChannels(reader, channelIdPrefix, tunerHostId); + return GetChannels(reader, channelIdPrefix, info.Id); } } @@ -47,20 +49,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - public Task<Stream> GetListingsStream(string url, CancellationToken cancellationToken) + public async Task<Stream> GetListingsStream(TunerHostInfo info, CancellationToken cancellationToken) { - if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + if (info.Url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) { - return _httpClient.Get(new HttpRequestOptions + using var requestMessage = new HttpRequestMessage(HttpMethod.Get, info.Url); + if (!string.IsNullOrEmpty(info.UserAgent)) { - Url = url, - CancellationToken = cancellationToken, - // Some data providers will require a user agent - UserAgent = _appHost.ApplicationUserAgent - }); + requestMessage.Headers.UserAgent.TryParseAdd(info.UserAgent); + } + + var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .SendAsync(requestMessage, cancellationToken) + .ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } - return Task.FromResult((Stream)File.OpenRead(url)); + return File.OpenRead(info.Url); } private const string ExtInfPrefix = "#EXTINF:"; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index bc4dcd894..2e1b89509 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -21,7 +21,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { public class SharedHttpStream : LiveStream, IDirectStreamProvider { - private readonly IHttpClient _httpClient; + private readonly IHttpClientFactory _httpClientFactory; private readonly IServerApplicationHost _appHost; public SharedHttpStream( @@ -29,14 +29,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts TunerHostInfo tunerHostInfo, string originalStreamId, IFileSystem fileSystem, - IHttpClient httpClient, + IHttpClientFactory httpClientFactory, ILogger logger, IConfigurationManager configurationManager, IServerApplicationHost appHost, IStreamHelper streamHelper) : base(mediaSource, tunerHostInfo, fileSystem, logger, configurationManager, streamHelper) { - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _appHost = appHost; OriginalStreamId = originalStreamId; EnableStreamSharing = true; @@ -55,25 +55,15 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts var typeName = GetType().Name; Logger.LogInformation("Opening " + typeName + " Live stream from {0}", url); - var httpRequestOptions = new HttpRequestOptions - { - Url = url, - CancellationToken = CancellationToken.None, - BufferContent = false, - DecompressionMethod = CompressionMethods.None - }; - - foreach (var header in mediaSource.RequiredHttpHeaders) - { - httpRequestOptions.RequestHeaders[header.Key] = header.Value; - } - - var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false); + // Response stream is disposed manually. + var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None) + .ConfigureAwait(false); var extension = "ts"; var requiresRemux = false; - var contentType = response.ContentType ?? string.Empty; + var contentType = response.Content.Headers.ContentType.ToString(); if (contentType.IndexOf("matroska", StringComparison.OrdinalIgnoreCase) != -1) { requiresRemux = true; @@ -132,24 +122,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) + public string GetFilePath() + { + return TempFilePath; + } + + private Task StartStreaming(HttpResponseMessage response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken) { return Task.Run(async () => { try { Logger.LogInformation("Beginning {0} stream to {1}", GetType().Name, TempFilePath); - using (response) - using (var stream = response.Content) - using (var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)) - { - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); - } + using var message = response; + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read); + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException ex) { diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json index e587c37d5..977a1c2d7 100644 --- a/Emby.Server.Implementations/Localization/Core/af.json +++ b/Emby.Server.Implementations/Localization/Core/af.json @@ -1,19 +1,19 @@ { "Artists": "Kunstenare", "Channels": "Kanale", - "Folders": "Fouers", - "Favorites": "Gunstelinge", + "Folders": "Lêergidse", + "Favorites": "Gunstellinge", "HeaderFavoriteShows": "Gunsteling Vertonings", "ValueSpecialEpisodeName": "Spesiale - {0}", "HeaderAlbumArtists": "Album Kunstenaars", "Books": "Boeke", "HeaderNextUp": "Volgende", - "Movies": "Rolprente", - "Shows": "Program", - "HeaderContinueWatching": "Hou Aan Kyk", + "Movies": "Flieks", + "Shows": "Televisie Reekse", + "HeaderContinueWatching": "Kyk Verder", "HeaderFavoriteEpisodes": "Gunsteling Episodes", "Photos": "Fotos", - "Playlists": "Speellysse", + "Playlists": "Snitlyste", "HeaderFavoriteArtists": "Gunsteling Kunstenaars", "HeaderFavoriteAlbums": "Gunsteling Albums", "Sync": "Sinkroniseer", @@ -23,7 +23,7 @@ "DeviceOfflineWithName": "{0} is ontkoppel", "Collections": "Versamelings", "Inherit": "Ontvang", - "HeaderLiveTV": "Live TV", + "HeaderLiveTV": "Lewendige TV", "Application": "Program", "AppDeviceValues": "App: {0}, Toestel: {1}", "VersionNumber": "Weergawe {0}", @@ -85,7 +85,6 @@ "ItemAddedWithName": "{0} is in die versameling", "HomeVideos": "Tuis opnames", "HeaderRecordingGroups": "Groep Opnames", - "HeaderCameraUploads": "Kamera Oplaai", "Genres": "Genres", "FailedLoginAttemptWithUserName": "Mislukte aansluiting van {0}", "ChapterNameValue": "Hoofstuk", @@ -95,5 +94,23 @@ "TasksChannelsCategory": "Internet kanale", "TasksApplicationCategory": "aansoek", "TasksLibraryCategory": "biblioteek", - "TasksMaintenanceCategory": "onderhoud" + "TasksMaintenanceCategory": "onderhoud", + "TaskCleanCacheDescription": "Vee kasregister lêers uit wat nie meer deur die stelsel benodig word nie.", + "TaskCleanCache": "Reinig Kasgeheue Lêergids", + "TaskDownloadMissingSubtitlesDescription": "Soek aanlyn vir vermiste onderskrifte gebasseer op metadata verstellings.", + "TaskDownloadMissingSubtitles": "Laai vermiste onderskrifte af", + "TaskRefreshChannelsDescription": "Vervris internet kanaal inligting.", + "TaskRefreshChannels": "Vervris Kanale", + "TaskCleanTranscodeDescription": "Vee transkodering lêers uit wat ouer is as een dag.", + "TaskCleanTranscode": "Reinig Transkoderings Leêrbinder", + "TaskUpdatePluginsDescription": "Laai opgedateerde inprop-sagteware af en installeer inprop-sagteware wat verstel is om outomaties op te dateer.", + "TaskUpdatePlugins": "Dateer Inprop-Sagteware Op", + "TaskRefreshPeopleDescription": "Vervris metadata oor akteurs en regisseurs in u media versameling.", + "TaskRefreshPeople": "Vervris Mense", + "TaskCleanLogsDescription": "Vee loglêers wat ouer as {0} dae is uit.", + "TaskCleanLogs": "Reinig Loglêer Lêervouer", + "TaskRefreshLibraryDescription": "Skandeer u media versameling vir nuwe lêers en verfris metadata.", + "TaskRefreshLibrary": "Skandeer Media Versameling", + "TaskRefreshChapterImagesDescription": "Maak kleinkiekeis (fotos) vir films wat hoofstukke het.", + "TaskRefreshChapterImages": "Verkry Hoofstuk Beelde" } diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json index 4eac8e75d..4b898e6fe 100644 --- a/Emby.Server.Implementations/Localization/Core/ar.json +++ b/Emby.Server.Implementations/Localization/Core/ar.json @@ -16,7 +16,6 @@ "Folders": "المجلدات", "Genres": "التضنيفات", "HeaderAlbumArtists": "فناني الألبومات", - "HeaderCameraUploads": "تحميلات الكاميرا", "HeaderContinueWatching": "استئناف", "HeaderFavoriteAlbums": "الألبومات المفضلة", "HeaderFavoriteArtists": "الفنانون المفضلون", diff --git a/Emby.Server.Implementations/Localization/Core/bg-BG.json b/Emby.Server.Implementations/Localization/Core/bg-BG.json index 3fc7c7dc0..1fed83276 100644 --- a/Emby.Server.Implementations/Localization/Core/bg-BG.json +++ b/Emby.Server.Implementations/Localization/Core/bg-BG.json @@ -16,7 +16,6 @@ "Folders": "Папки", "Genres": "Жанрове", "HeaderAlbumArtists": "Изпълнители на албуми", - "HeaderCameraUploads": "Качени от камера", "HeaderContinueWatching": "Продължаване на гледането", "HeaderFavoriteAlbums": "Любими албуми", "HeaderFavoriteArtists": "Любими изпълнители", diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json index 1bd190982..5667bf337 100644 --- a/Emby.Server.Implementations/Localization/Core/bn.json +++ b/Emby.Server.Implementations/Localization/Core/bn.json @@ -14,7 +14,6 @@ "HeaderFavoriteArtists": "প্রিয় শিল্পীরা", "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো", "HeaderContinueWatching": "দেখতে থাকুন", - "HeaderCameraUploads": "ক্যামেরার আপলোড সমূহ", "HeaderAlbumArtists": "এলবাম শিল্পী", "Genres": "জেনার", "Folders": "ফোল্ডারগুলো", diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 2c802a39e..b7852eccb 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -16,7 +16,6 @@ "Folders": "Carpetes", "Genres": "Gèneres", "HeaderAlbumArtists": "Artistes del Àlbum", - "HeaderCameraUploads": "Pujades de Càmera", "HeaderContinueWatching": "Continua Veient", "HeaderFavoriteAlbums": "Àlbums Preferits", "HeaderFavoriteArtists": "Artistes Preferits", diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 464ca28ca..fb31b01ff 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -16,7 +16,6 @@ "Folders": "Složky", "Genres": "Žánry", "HeaderAlbumArtists": "Umělci alba", - "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovat ve sledování", "HeaderFavoriteAlbums": "Oblíbená alba", "HeaderFavoriteArtists": "Oblíbení interpreti", @@ -114,5 +113,7 @@ "TasksChannelsCategory": "Internetové kanály", "TasksApplicationCategory": "Aplikace", "TasksLibraryCategory": "Knihovna", - "TasksMaintenanceCategory": "Údržba" + "TasksMaintenanceCategory": "Údržba", + "TaskCleanActivityLogDescription": "Smazat záznamy o aktivitě, které jsou starší než zadaná doba.", + "TaskCleanActivityLog": "Smazat záznam aktivity" } diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json index f5397b62c..b29ad94ef 100644 --- a/Emby.Server.Implementations/Localization/Core/da.json +++ b/Emby.Server.Implementations/Localization/Core/da.json @@ -16,7 +16,6 @@ "Folders": "Mapper", "Genres": "Genrer", "HeaderAlbumArtists": "Albumkunstnere", - "HeaderCameraUploads": "Kamera Uploads", "HeaderContinueWatching": "Fortsæt Afspilning", "HeaderFavoriteAlbums": "Favoritalbummer", "HeaderFavoriteArtists": "Favoritkunstnere", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index fcbe9566e..c81de8218 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -16,7 +16,6 @@ "Folders": "Verzeichnisse", "Genres": "Genres", "HeaderAlbumArtists": "Album-Interpreten", - "HeaderCameraUploads": "Kamera-Uploads", "HeaderContinueWatching": "Fortsetzen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Interpreten", @@ -114,5 +113,7 @@ "TasksChannelsCategory": "Internet Kanäle", "TasksApplicationCategory": "Anwendung", "TasksLibraryCategory": "Bibliothek", - "TasksMaintenanceCategory": "Wartung" + "TasksMaintenanceCategory": "Wartung", + "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.", + "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 0753ea39d..c45cc11cb 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -16,7 +16,6 @@ "Folders": "Φάκελοι", "Genres": "Είδη", "HeaderAlbumArtists": "Καλλιτέχνες του Άλμπουμ", - "HeaderCameraUploads": "Μεταφορτώσεις Κάμερας", "HeaderContinueWatching": "Συνεχίστε την παρακολούθηση", "HeaderFavoriteAlbums": "Αγαπημένα Άλμπουμ", "HeaderFavoriteArtists": "Αγαπημένοι Καλλιτέχνες", diff --git a/Emby.Server.Implementations/Localization/Core/en-GB.json b/Emby.Server.Implementations/Localization/Core/en-GB.json index 544c38cfa..57ff13219 100644 --- a/Emby.Server.Implementations/Localization/Core/en-GB.json +++ b/Emby.Server.Implementations/Localization/Core/en-GB.json @@ -16,7 +16,6 @@ "Folders": "Folders", "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favourite Albums", "HeaderFavoriteArtists": "Favourite Artists", diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 97a843160..6d8b222b4 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -16,7 +16,6 @@ "Folders": "Folders", "Genres": "Genres", "HeaderAlbumArtists": "Album Artists", - "HeaderCameraUploads": "Camera Uploads", "HeaderContinueWatching": "Continue Watching", "HeaderFavoriteAlbums": "Favorite Albums", "HeaderFavoriteArtists": "Favorite Artists", @@ -96,6 +95,8 @@ "TasksLibraryCategory": "Library", "TasksApplicationCategory": "Application", "TasksChannelsCategory": "Internet Channels", + "TaskCleanActivityLog": "Clean Activity Log", + "TaskCleanActivityLogDescription": "Deletes activity log entries older than the configured age.", "TaskCleanCache": "Clean Cache Directory", "TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.", "TaskRefreshChapterImages": "Extract Chapter Images", diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json index ac96c788c..390074cdd 100644 --- a/Emby.Server.Implementations/Localization/Core/es-AR.json +++ b/Emby.Server.Implementations/Localization/Core/es-AR.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas de álbum", - "HeaderCameraUploads": "Subidas de cámara", "HeaderContinueWatching": "Seguir viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 4ba324aa1..ab54c0ea6 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidas desde la cámara", "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index e7bd3959b..60abc08d4 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -16,7 +16,6 @@ "Folders": "Carpetas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas del álbum", - "HeaderCameraUploads": "Subidas desde la cámara", "HeaderContinueWatching": "Continuar viendo", "HeaderFavoriteAlbums": "Álbumes favoritos", "HeaderFavoriteArtists": "Artistas favoritos", @@ -78,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Fallo de descarga de subtítulos desde {0} para {1}", "Sync": "Sincronizar", "System": "Sistema", - "TvShows": "Programas de televisión", + "TvShows": "Series", "User": "Usuario", "UserCreatedWithName": "El usuario {0} ha sido creado", "UserDeletedWithName": "El usuario {0} ha sido borrado", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index 0959ef2ca..dcd30694f 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -105,7 +105,6 @@ "Inherit": "Heredar", "HomeVideos": "Videos caseros", "HeaderRecordingGroups": "Grupos de grabación", - "HeaderCameraUploads": "Subidas desde la cámara", "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", "DeviceOnlineWithName": "{0} está conectado", "DeviceOfflineWithName": "{0} se ha desconectado", diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 0ef16542f..b64ffbfbb 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -12,10 +12,12 @@ "Application": "Aplicación", "AppDeviceValues": "App: {0}, Dispositivo: {1}", "HeaderContinueWatching": "Continuar Viendo", - "HeaderCameraUploads": "Subidas de Cámara", "HeaderAlbumArtists": "Artistas del Álbum", "Genres": "Géneros", "Folders": "Carpetas", "Favorites": "Favoritos", - "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}" + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}", + "HeaderFavoriteSongs": "Canciones Favoritas", + "HeaderFavoriteEpisodes": "Episodios Favoritos", + "HeaderFavoriteArtists": "Artistas Favoritos" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 500c29217..1986decf0 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -16,7 +16,6 @@ "Folders": "پوشهها", "Genres": "ژانرها", "HeaderAlbumArtists": "هنرمندان آلبوم", - "HeaderCameraUploads": "آپلودهای دوربین", "HeaderContinueWatching": "ادامه تماشا", "HeaderFavoriteAlbums": "آلبومهای مورد علاقه", "HeaderFavoriteArtists": "هنرمندان مورد علاقه", diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index f8d6e0e09..8e219a9ce 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -1,7 +1,7 @@ { "HeaderLiveTV": "Live-TV", "NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.", - "NameSeasonUnknown": "Tuntematon Kausi", + "NameSeasonUnknown": "Tuntematon kausi", "NameSeasonNumber": "Kausi {0}", "NameInstallFailed": "{0} asennus epäonnistui", "MusicVideos": "Musiikkivideot", @@ -19,24 +19,23 @@ "ItemAddedWithName": "{0} lisättiin kirjastoon", "Inherit": "Periytyä", "HomeVideos": "Kotivideot", - "HeaderRecordingGroups": "Nauhoiteryhmät", + "HeaderRecordingGroups": "Tallennusryhmät", "HeaderNextUp": "Seuraavaksi", - "HeaderFavoriteSongs": "Lempikappaleet", - "HeaderFavoriteShows": "Lempisarjat", - "HeaderFavoriteEpisodes": "Lempijaksot", - "HeaderCameraUploads": "Kamerasta Lähetetyt", - "HeaderFavoriteArtists": "Lempiartistit", - "HeaderFavoriteAlbums": "Lempialbumit", + "HeaderFavoriteSongs": "Suosikkikappaleet", + "HeaderFavoriteShows": "Suosikkisarjat", + "HeaderFavoriteEpisodes": "Suosikkijaksot", + "HeaderFavoriteArtists": "Suosikkiartistit", + "HeaderFavoriteAlbums": "Suosikkialbumit", "HeaderContinueWatching": "Jatka katsomista", - "HeaderAlbumArtists": "Albumin esittäjä", + "HeaderAlbumArtists": "Albumin artistit", "Genres": "Tyylilajit", "Folders": "Kansiot", "Favorites": "Suosikit", "FailedLoginAttemptWithUserName": "Kirjautuminen epäonnistui kohteesta {0}", "DeviceOnlineWithName": "{0} on yhdistetty", - "DeviceOfflineWithName": "{0} on katkaissut yhteytensä", + "DeviceOfflineWithName": "{0} yhteys on katkaistu", "Collections": "Kokoelmat", - "ChapterNameValue": "Luku: {0}", + "ChapterNameValue": "Jakso: {0}", "Channels": "Kanavat", "CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}", "Books": "Kirjat", @@ -62,25 +61,25 @@ "UserPolicyUpdatedWithName": "Käyttöoikeudet päivitetty käyttäjälle {0}", "UserPasswordChangedWithName": "Salasana vaihdettu käyttäjälle {0}", "UserOnlineFromDevice": "{0} on paikalla osoitteesta {1}", - "UserOfflineFromDevice": "{0} yhteys katkaistu {1}", + "UserOfflineFromDevice": "{0} yhteys katkaistu kohteesta {1}", "UserLockedOutWithName": "Käyttäjä {0} lukittu", "UserDownloadingItemWithValues": "{0} lataa {1}", "UserDeletedWithName": "Käyttäjä {0} poistettu", "UserCreatedWithName": "Käyttäjä {0} luotu", - "TvShows": "TV-sarjat", + "TvShows": "TV-ohjelmat", "Sync": "Synkronoi", - "SubtitleDownloadFailureFromForItem": "Tekstitysten lataus ({0} -> {1}) epäonnistui //this string would have to be generated for each provider and movie because of finnish cases, sorry", - "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Kokeile hetken kuluttua uudelleen.", + "SubtitleDownloadFailureFromForItem": "Tekstitystä ei voitu ladata osoitteesta {0} kohteelle {1}", + "StartupEmbyServerIsLoading": "Jellyfin palvelin latautuu. Yritä hetken kuluttua uudelleen.", "Songs": "Kappaleet", - "Shows": "Sarjat", - "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen", + "Shows": "Ohjelmat", + "ServerNameNeedsToBeRestarted": "{0} on käynnistettävä uudelleen", "ProviderValue": "Tarjoaja: {0}", "Plugin": "Liitännäinen", "NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty", "NotificationOptionVideoPlayback": "Videota toistetaan", "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos", "NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui", - "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen", + "NotificationOptionServerRestartRequired": "Palvelin on käynnistettävä uudelleen", "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty", "NotificationOptionPluginUninstalled": "Liitännäinen poistettu", "NotificationOptionPluginInstalled": "Liitännäinen asennettu", @@ -105,10 +104,10 @@ "TaskRefreshPeople": "Päivitä henkilöt", "TaskCleanLogsDescription": "Poistaa lokitiedostot jotka ovat yli {0} päivää vanhoja.", "TaskCleanLogs": "Puhdista lokihakemisto", - "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uusien tiedostojen varalle, sekä virkistää metatiedot.", + "TaskRefreshLibraryDescription": "Skannaa mediakirjastosi uudet tiedostot ja päivittää metatiedot.", "TaskRefreshLibrary": "Skannaa mediakirjasto", - "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on lukuja.", - "TaskRefreshChapterImages": "Eristä lukujen kuvat", + "TaskRefreshChapterImagesDescription": "Luo pienoiskuvat videoille joissa on jaksoja.", + "TaskRefreshChapterImages": "Pura jakson kuvat", "TaskCleanCacheDescription": "Poistaa järjestelmälle tarpeettomat väliaikaistiedostot.", "TaskCleanCache": "Tyhjennä välimuisti-hakemisto", "TasksChannelsCategory": "Internet kanavat", diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 47daf2044..1a3e18832 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -73,7 +73,6 @@ "HeaderFavoriteArtists": "Paboritong Artista", "HeaderFavoriteAlbums": "Paboritong Albums", "HeaderContinueWatching": "Ituloy Manood", - "HeaderCameraUploads": "Camera Uploads", "HeaderAlbumArtists": "Artista ng Album", "Genres": "Kategorya", "Folders": "Folders", diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json index cd1c8144f..3d7592e3c 100644 --- a/Emby.Server.Implementations/Localization/Core/fr-CA.json +++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json @@ -16,7 +16,6 @@ "Folders": "Dossiers", "Genres": "Genres", "HeaderAlbumArtists": "Artistes de l'album", - "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes favoris", diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 47ebe1254..cc9243f37 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -16,7 +16,6 @@ "Folders": "Dossiers", "Genres": "Genres", "HeaderAlbumArtists": "Artistes", - "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", "HeaderFavoriteArtists": "Artistes préférés", @@ -107,12 +106,14 @@ "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", - "TaskRefreshLibrary": "Scanner toute les Bibliothèques", + "TaskRefreshLibrary": "Scanner toutes les Bibliothèques", "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCache": "Vider le répertoire cache", "TasksApplicationCategory": "Application", "TasksLibraryCategory": "Bibliothèque", - "TasksMaintenanceCategory": "Maintenance" + "TasksMaintenanceCategory": "Maintenance", + "TaskCleanActivityLogDescription": "Supprime les entrées du journal d'activité antérieures à l'âge configuré.", + "TaskCleanActivityLog": "Nettoyer le journal d'activité" } diff --git a/Emby.Server.Implementations/Localization/Core/gl.json b/Emby.Server.Implementations/Localization/Core/gl.json index 94034962d..faee2519a 100644 --- a/Emby.Server.Implementations/Localization/Core/gl.json +++ b/Emby.Server.Implementations/Localization/Core/gl.json @@ -1,3 +1,11 @@ { - "Albums": "Álbumes" + "Albums": "Álbumes", + "Collections": "Colecións", + "ChapterNameValue": "Capítulos {0}", + "Channels": "Canles", + "CameraImageUploadedFrom": "Cargouse unha nova imaxe da cámara desde {0}", + "Books": "Libros", + "AuthenticationSucceededWithUserName": "{0} autenticouse correctamente", + "Artists": "Artistas", + "Application": "Aplicativo" } diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json index 8780a884b..ee1f8775e 100644 --- a/Emby.Server.Implementations/Localization/Core/gsw.json +++ b/Emby.Server.Implementations/Localization/Core/gsw.json @@ -16,7 +16,6 @@ "Folders": "Ordner", "Genres": "Genres", "HeaderAlbumArtists": "Album-Künstler", - "HeaderCameraUploads": "Kamera-Uploads", "HeaderContinueWatching": "weiter schauen", "HeaderFavoriteAlbums": "Lieblingsalben", "HeaderFavoriteArtists": "Lieblings-Künstler", diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json index dc3a98154..f906d6e11 100644 --- a/Emby.Server.Implementations/Localization/Core/he.json +++ b/Emby.Server.Implementations/Localization/Core/he.json @@ -16,7 +16,6 @@ "Folders": "תיקיות", "Genres": "ז'אנרים", "HeaderAlbumArtists": "אמני האלבום", - "HeaderCameraUploads": "העלאות ממצלמה", "HeaderContinueWatching": "המשך לצפות", "HeaderFavoriteAlbums": "אלבומים מועדפים", "HeaderFavoriteArtists": "אמנים מועדפים", diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json new file mode 100644 index 000000000..df68d3bbd --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -0,0 +1,3 @@ +{ + "Albums": "आल्बुम्" +} diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json index 97c77017b..9be91b724 100644 --- a/Emby.Server.Implementations/Localization/Core/hr.json +++ b/Emby.Server.Implementations/Localization/Core/hr.json @@ -5,18 +5,17 @@ "Artists": "Izvođači", "AuthenticationSucceededWithUserName": "{0} uspješno ovjerena", "Books": "Knjige", - "CameraImageUploadedFrom": "Nova fotografija sa kamere je uploadana iz {0}", + "CameraImageUploadedFrom": "Nova fotografija sa kamere je učitana iz {0}", "Channels": "Kanali", "ChapterNameValue": "Poglavlje {0}", "Collections": "Kolekcije", - "DeviceOfflineWithName": "{0} se odspojilo", - "DeviceOnlineWithName": "{0} je spojeno", - "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave za {0}", + "DeviceOfflineWithName": "{0} je prekinuo vezu", + "DeviceOnlineWithName": "{0} je povezan", + "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}", "Favorites": "Favoriti", "Folders": "Mape", "Genres": "Žanrovi", "HeaderAlbumArtists": "Izvođači na albumu", - "HeaderCameraUploads": "Uvoz sa kamere", "HeaderContinueWatching": "Nastavi gledati", "HeaderFavoriteAlbums": "Omiljeni albumi", "HeaderFavoriteArtists": "Omiljeni izvođači", @@ -24,95 +23,97 @@ "HeaderFavoriteShows": "Omiljene serije", "HeaderFavoriteSongs": "Omiljene pjesme", "HeaderLiveTV": "TV uživo", - "HeaderNextUp": "Sljedeće je", + "HeaderNextUp": "Slijedi", "HeaderRecordingGroups": "Grupa snimka", - "HomeVideos": "Kućni videi", + "HomeVideos": "Kućni video", "Inherit": "Naslijedi", "ItemAddedWithName": "{0} je dodano u biblioteku", - "ItemRemovedWithName": "{0} je uklonjen iz biblioteke", + "ItemRemovedWithName": "{0} je uklonjeno iz biblioteke", "LabelIpAddressValue": "IP adresa: {0}", "LabelRunningTimeValue": "Vrijeme rada: {0}", "Latest": "Najnovije", - "MessageApplicationUpdated": "Jellyfin Server je ažuriran", - "MessageApplicationUpdatedTo": "Jellyfin Server je ažuriran na {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Odjeljak postavka servera {0} je ažuriran", - "MessageServerConfigurationUpdated": "Postavke servera su ažurirane", + "MessageApplicationUpdated": "Jellyfin server je ažuriran", + "MessageApplicationUpdatedTo": "Jellyfin server je ažuriran na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Dio konfiguracije servera {0} je ažuriran", + "MessageServerConfigurationUpdated": "Konfiguracija servera je ažurirana", "MixedContent": "Miješani sadržaj", "Movies": "Filmovi", "Music": "Glazba", "MusicVideos": "Glazbeni spotovi", "NameInstallFailed": "{0} neuspješnih instalacija", "NameSeasonNumber": "Sezona {0}", - "NameSeasonUnknown": "Nepoznata sezona", + "NameSeasonUnknown": "Sezona nepoznata", "NewVersionIsAvailable": "Nova verzija Jellyfin servera je dostupna za preuzimanje.", - "NotificationOptionApplicationUpdateAvailable": "Dostupno ažuriranje aplikacije", - "NotificationOptionApplicationUpdateInstalled": "Instalirano ažuriranje aplikacije", - "NotificationOptionAudioPlayback": "Reprodukcija glazbe započeta", - "NotificationOptionAudioPlaybackStopped": "Reprodukcija audiozapisa je zaustavljena", - "NotificationOptionCameraImageUploaded": "Slike kamere preuzete", - "NotificationOptionInstallationFailed": "Instalacija neuspješna", - "NotificationOptionNewLibraryContent": "Novi sadržaj je dodan", - "NotificationOptionPluginError": "Dodatak otkazao", + "NotificationOptionApplicationUpdateAvailable": "Dostupno je ažuriranje aplikacije", + "NotificationOptionApplicationUpdateInstalled": "Instalirano je ažuriranje aplikacije", + "NotificationOptionAudioPlayback": "Reprodukcija glazbe započela", + "NotificationOptionAudioPlaybackStopped": "Reprodukcija glazbe zaustavljena", + "NotificationOptionCameraImageUploaded": "Slika s kamere učitana", + "NotificationOptionInstallationFailed": "Instalacija nije uspjela", + "NotificationOptionNewLibraryContent": "Novi sadržaj dodan", + "NotificationOptionPluginError": "Dodatak zakazao", "NotificationOptionPluginInstalled": "Dodatak instaliran", - "NotificationOptionPluginUninstalled": "Dodatak uklonjen", - "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje za dodatak", - "NotificationOptionServerRestartRequired": "Potrebno ponovo pokretanje servera", - "NotificationOptionTaskFailed": "Zakazan zadatak nije izvršen", + "NotificationOptionPluginUninstalled": "Dodatak deinstaliran", + "NotificationOptionPluginUpdateInstalled": "Instalirano ažuriranje dodatka", + "NotificationOptionServerRestartRequired": "Ponovno pokrenite server", + "NotificationOptionTaskFailed": "Greška zakazanog zadatka", "NotificationOptionUserLockedOut": "Korisnik zaključan", - "NotificationOptionVideoPlayback": "Reprodukcija videa započeta", - "NotificationOptionVideoPlaybackStopped": "Reprodukcija videozapisa je zaustavljena", - "Photos": "Slike", - "Playlists": "Popis za reprodukciju", + "NotificationOptionVideoPlayback": "Reprodukcija videa započela", + "NotificationOptionVideoPlaybackStopped": "Reprodukcija videa zaustavljena", + "Photos": "Fotografije", + "Playlists": "Popisi za reprodukciju", "Plugin": "Dodatak", "PluginInstalledWithName": "{0} je instalirano", "PluginUninstalledWithName": "{0} je deinstalirano", "PluginUpdatedWithName": "{0} je ažurirano", - "ProviderValue": "Pružitelj: {0}", + "ProviderValue": "Pružatelj: {0}", "ScheduledTaskFailedWithName": "{0} neuspjelo", "ScheduledTaskStartedWithName": "{0} pokrenuto", - "ServerNameNeedsToBeRestarted": "{0} treba biti ponovno pokrenuto", + "ServerNameNeedsToBeRestarted": "{0} treba ponovno pokrenuti", "Shows": "Serije", "Songs": "Pjesme", - "StartupEmbyServerIsLoading": "Jellyfin Server se učitava. Pokušajte ponovo kasnije.", + "StartupEmbyServerIsLoading": "Jellyfin server se učitava. Pokušajte ponovo uskoro.", "SubtitleDownloadFailureForItem": "Titlovi prijevoda nisu preuzeti za {0}", - "SubtitleDownloadFailureFromForItem": "Prijevodi nisu uspješno preuzeti {0} od {1}", - "Sync": "Sink.", - "System": "Sistem", + "SubtitleDownloadFailureFromForItem": "Prijevod nije uspješno preuzet od {0} za {1}", + "Sync": "Sinkronizacija", + "System": "Sustav", "TvShows": "Serije", "User": "Korisnik", - "UserCreatedWithName": "Korisnik {0} je stvoren", + "UserCreatedWithName": "Korisnik {0} je kreiran", "UserDeletedWithName": "Korisnik {0} je obrisan", - "UserDownloadingItemWithValues": "{0} se preuzima {1}", + "UserDownloadingItemWithValues": "{0} preuzima {1}", "UserLockedOutWithName": "Korisnik {0} je zaključan", - "UserOfflineFromDevice": "{0} se odspojilo od {1}", - "UserOnlineFromDevice": "{0} je online od {1}", + "UserOfflineFromDevice": "{0} prekinuo vezu od {1}", + "UserOnlineFromDevice": "{0} povezan od {1}", "UserPasswordChangedWithName": "Lozinka je promijenjena za korisnika {0}", - "UserPolicyUpdatedWithName": "Pravila za korisnika su ažurirana za {0}", - "UserStartedPlayingItemWithValues": "{0} je pokrenuo {1}", - "UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}", + "UserPolicyUpdatedWithName": "Pravila za korisnika ažurirana su za {0}", + "UserStartedPlayingItemWithValues": "{0} je pokrenuo reprodukciju {1} na {2}", + "UserStoppedPlayingItemWithValues": "{0} je završio reprodukciju {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je dodano u medijsku biblioteku", - "ValueSpecialEpisodeName": "Specijal - {0}", + "ValueSpecialEpisodeName": "Posebno - {0}", "VersionNumber": "Verzija {0}", - "TaskRefreshLibraryDescription": "Skenira vašu medijsku knjižnicu sa novim datotekama i osvježuje metapodatke.", - "TaskRefreshLibrary": "Skeniraj medijsku knjižnicu", - "TaskRefreshChapterImagesDescription": "Stvara sličice za videozapise koji imaju poglavlja.", - "TaskRefreshChapterImages": "Raspakiraj slike poglavlja", - "TaskCleanCacheDescription": "Briše priručne datoteke nepotrebne za sistem.", - "TaskCleanCache": "Očisti priručnu memoriju", + "TaskRefreshLibraryDescription": "Skenira medijsku biblioteku radi novih datoteka i osvježava metapodatke.", + "TaskRefreshLibrary": "Skeniraj medijsku biblioteku", + "TaskRefreshChapterImagesDescription": "Kreira sličice za videozapise koji imaju poglavlja.", + "TaskRefreshChapterImages": "Izdvoji slike poglavlja", + "TaskCleanCacheDescription": "Briše nepotrebne datoteke iz predmemorije.", + "TaskCleanCache": "Očisti mapu predmemorije", "TasksApplicationCategory": "Aplikacija", "TasksMaintenanceCategory": "Održavanje", - "TaskDownloadMissingSubtitlesDescription": "Pretraživanje interneta za prijevodima koji nedostaju bazirano na konfiguraciji meta podataka.", - "TaskDownloadMissingSubtitles": "Preuzimanje prijevoda koji nedostaju", - "TaskRefreshChannelsDescription": "Osvježava informacije o internet kanalima.", + "TaskDownloadMissingSubtitlesDescription": "Pretraži Internet za prijevodima koji nedostaju prema konfiguraciji metapodataka.", + "TaskDownloadMissingSubtitles": "Preuzmi prijevod koji nedostaje", + "TaskRefreshChannelsDescription": "Osvježava informacije Internet kanala.", "TaskRefreshChannels": "Osvježi kanale", - "TaskCleanTranscodeDescription": "Briše transkodirane fajlove starije od jednog dana.", - "TaskCleanTranscode": "Očisti direktorij za transkodiranje", - "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su podešeni da se ažuriraju automatski.", + "TaskCleanTranscodeDescription": "Briše transkodirane datoteke starije od jednog dana.", + "TaskCleanTranscode": "Očisti mapu transkodiranja", + "TaskUpdatePluginsDescription": "Preuzima i instalira ažuriranja za dodatke koji su konfigurirani da se ažuriraju automatski.", "TaskUpdatePlugins": "Ažuriraj dodatke", - "TaskRefreshPeopleDescription": "Ažurira meta podatke za glumce i redatelje u vašoj medijskoj biblioteci.", - "TaskRefreshPeople": "Osvježi ljude", - "TaskCleanLogsDescription": "Briši logove koji su stariji od {0} dana.", - "TaskCleanLogs": "Očisti direktorij sa logovima", + "TaskRefreshPeopleDescription": "Ažurira metapodatke za glumce i redatelje u medijskoj biblioteci.", + "TaskRefreshPeople": "Osvježi osobe", + "TaskCleanLogsDescription": "Briše zapise dnevnika koji su stariji od {0} dana.", + "TaskCleanLogs": "Očisti mapu dnevnika zapisa", "TasksChannelsCategory": "Internet kanali", - "TasksLibraryCategory": "Biblioteka" + "TasksLibraryCategory": "Biblioteka", + "TaskCleanActivityLogDescription": "Briše zapise dnevnika aktivnosti starije od navedenog vremena.", + "TaskCleanActivityLog": "Očisti dnevnik aktivnosti" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index c5c3844e3..343d213d4 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -16,7 +16,6 @@ "Folders": "Könyvtárak", "Genres": "Műfajok", "HeaderAlbumArtists": "Album előadók", - "HeaderCameraUploads": "Kamera feltöltések", "HeaderContinueWatching": "Megtekintés folytatása", "HeaderFavoriteAlbums": "Kedvenc albumok", "HeaderFavoriteArtists": "Kedvenc előadók", diff --git a/Emby.Server.Implementations/Localization/Core/id.json b/Emby.Server.Implementations/Localization/Core/id.json index ccb72ff93..ef3ed2580 100644 --- a/Emby.Server.Implementations/Localization/Core/id.json +++ b/Emby.Server.Implementations/Localization/Core/id.json @@ -1,7 +1,7 @@ { "Albums": "Album", "AuthenticationSucceededWithUserName": "{0} berhasil diautentikasi", - "AppDeviceValues": "Aplikasi: {0}, Alat: {1}", + "AppDeviceValues": "Aplikasi : {0}, Alat : {1}", "LabelRunningTimeValue": "Waktu berjalan: {0}", "MessageApplicationUpdatedTo": "Jellyfin Server sudah diperbarui ke {0}", "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui", @@ -19,10 +19,9 @@ "HeaderFavoriteEpisodes": "Episode Favorit", "HeaderFavoriteArtists": "Artis Favorit", "HeaderFavoriteAlbums": "Album Favorit", - "HeaderContinueWatching": "Lanjutkan Menonton", - "HeaderCameraUploads": "Unggahan Kamera", + "HeaderContinueWatching": "Lanjut Menonton", "HeaderAlbumArtists": "Album Artis", - "Genres": "Genre", + "Genres": "Aliran", "Folders": "Folder", "Favorites": "Favorit", "Collections": "Koleksi", diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index 0f0f9130b..0f769eaad 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -13,7 +13,6 @@ "HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", - "HeaderCameraUploads": "Myndavéla upphal", "HeaderAlbumArtists": "Höfundur plötu", "Genres": "Tegundir", "Folders": "Möppur", diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json index bf1a0ef13..9e37ddc27 100644 --- a/Emby.Server.Implementations/Localization/Core/it.json +++ b/Emby.Server.Implementations/Localization/Core/it.json @@ -16,7 +16,6 @@ "Folders": "Cartelle", "Genres": "Generi", "HeaderAlbumArtists": "Artisti degli Album", - "HeaderCameraUploads": "Caricamenti Fotocamera", "HeaderContinueWatching": "Continua a guardare", "HeaderFavoriteAlbums": "Album Preferiti", "HeaderFavoriteArtists": "Artisti Preferiti", @@ -114,5 +113,7 @@ "TasksChannelsCategory": "Canali su Internet", "TasksApplicationCategory": "Applicazione", "TasksLibraryCategory": "Libreria", - "TasksMaintenanceCategory": "Manutenzione" + "TasksMaintenanceCategory": "Manutenzione", + "TaskCleanActivityLog": "Attività di Registro Completate", + "TaskCleanActivityLogDescription": "Elimina gli inserimenti nel registro delle attività più vecchie dell’età configurata." } diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json index a4d9f9ef6..02bf8496f 100644 --- a/Emby.Server.Implementations/Localization/Core/ja.json +++ b/Emby.Server.Implementations/Localization/Core/ja.json @@ -16,7 +16,6 @@ "Folders": "フォルダー", "Genres": "ジャンル", "HeaderAlbumArtists": "アルバムアーティスト", - "HeaderCameraUploads": "カメラアップロード", "HeaderContinueWatching": "視聴を続ける", "HeaderFavoriteAlbums": "お気に入りのアルバム", "HeaderFavoriteArtists": "お気に入りのアーティスト", @@ -97,7 +96,7 @@ "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータをリフレッシュします。", "TaskRefreshLibrary": "メディアライブラリのスキャン", "TaskCleanCacheDescription": "不要なキャッシュを消去します。", - "TaskCleanCache": "キャッシュの掃除", + "TaskCleanCache": "キャッシュを消去", "TasksChannelsCategory": "ネットチャンネル", "TasksApplicationCategory": "アプリケーション", "TasksLibraryCategory": "ライブラリ", @@ -113,5 +112,7 @@ "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。", "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。", "TaskRefreshChapterImages": "チャプター画像を抽出する", - "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする" + "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする", + "TaskCleanActivityLogDescription": "設定された期間よりも古いアクティビティの履歴を削除します。", + "TaskCleanActivityLog": "アクティビティの履歴を消去" } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 5618ff4a8..91c1fb15b 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -16,7 +16,6 @@ "Folders": "Qaltalar", "Genres": "Janrlar", "HeaderAlbumArtists": "Álbom oryndaýshylary", - "HeaderCameraUploads": "Kameradan júktelgender", "HeaderContinueWatching": "Qaraýdy jalǵastyrý", "HeaderFavoriteAlbums": "Tańdaýly álbomdar", "HeaderFavoriteArtists": "Tańdaýly oryndaýshylar", diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json index 9e3ecd5a8..b8b39833c 100644 --- a/Emby.Server.Implementations/Localization/Core/ko.json +++ b/Emby.Server.Implementations/Localization/Core/ko.json @@ -16,7 +16,6 @@ "Folders": "폴더", "Genres": "장르", "HeaderAlbumArtists": "앨범 아티스트", - "HeaderCameraUploads": "카메라 업로드", "HeaderContinueWatching": "계속 시청하기", "HeaderFavoriteAlbums": "즐겨찾는 앨범", "HeaderFavoriteArtists": "즐겨찾는 아티스트", @@ -28,7 +27,7 @@ "HeaderRecordingGroups": "녹화 그룹", "HomeVideos": "홈 비디오", "Inherit": "상속", - "ItemAddedWithName": "{0}가 라이브러리에 추가됨", + "ItemAddedWithName": "{0}가 라이브러리에 추가되었습니다", "ItemRemovedWithName": "{0}가 라이브러리에서 제거됨", "LabelIpAddressValue": "IP 주소: {0}", "LabelRunningTimeValue": "상영 시간: {0}", @@ -84,8 +83,8 @@ "UserDeletedWithName": "사용자 {0} 삭제됨", "UserDownloadingItemWithValues": "{0}이(가) {1}을 다운로드 중입니다", "UserLockedOutWithName": "유저 {0} 은(는) 잠금처리 되었습니다", - "UserOfflineFromDevice": "{1}로부터 {0}의 연결이 끊겼습니다", - "UserOnlineFromDevice": "{0}은 {1}에서 온라인 상태입니다", + "UserOfflineFromDevice": "{1}에서 {0}의 연결이 끊킴", + "UserOnlineFromDevice": "{0}이 {1}으로 접속", "UserPasswordChangedWithName": "사용자 {0}의 비밀번호가 변경되었습니다", "UserPolicyUpdatedWithName": "{0}의 사용자 정책이 업데이트되었습니다", "UserStartedPlayingItemWithValues": "{2}에서 {0}이 {1} 재생 중", @@ -114,5 +113,7 @@ "TaskCleanCacheDescription": "시스템에서 더 이상 필요하지 않은 캐시 파일을 삭제합니다.", "TaskCleanCache": "캐시 폴더 청소", "TasksChannelsCategory": "인터넷 채널", - "TasksLibraryCategory": "라이브러리" + "TasksLibraryCategory": "라이브러리", + "TaskCleanActivityLogDescription": "구성된 기간보다 오래된 활동내역 삭제.", + "TaskCleanActivityLog": "활동내역청소" } diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json index 35053766b..d4cb592ef 100644 --- a/Emby.Server.Implementations/Localization/Core/lt-LT.json +++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json @@ -16,7 +16,6 @@ "Folders": "Katalogai", "Genres": "Žanrai", "HeaderAlbumArtists": "Albumo atlikėjai", - "HeaderCameraUploads": "Kameros", "HeaderContinueWatching": "Žiūrėti toliau", "HeaderFavoriteAlbums": "Mėgstami Albumai", "HeaderFavoriteArtists": "Mėgstami Atlikėjai", diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index dbcf17287..5e3acfbe9 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -72,7 +72,6 @@ "ItemAddedWithName": "{0} tika pievienots bibliotēkai", "HeaderLiveTV": "Tiešraides TV", "HeaderContinueWatching": "Turpināt Skatīšanos", - "HeaderCameraUploads": "Kameras augšupielādes", "HeaderAlbumArtists": "Albumu Izpildītāji", "Genres": "Žanri", "Folders": "Mapes", diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json index bbdf99aba..b780ef498 100644 --- a/Emby.Server.Implementations/Localization/Core/mk.json +++ b/Emby.Server.Implementations/Localization/Core/mk.json @@ -51,7 +51,6 @@ "HeaderFavoriteArtists": "Омилени Изведувачи", "HeaderFavoriteAlbums": "Омилени Албуми", "HeaderContinueWatching": "Продолжи со гледање", - "HeaderCameraUploads": "Поставувања од камера", "HeaderAlbumArtists": "Изведувачи од Албуми", "Genres": "Жанрови", "Folders": "Папки", diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index b6db2b0f2..fdb4171b5 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -54,7 +54,6 @@ "ItemAddedWithName": "{0} हे संग्रहालयात जोडले गेले", "HomeVideos": "घरचे व्हिडीयो", "HeaderRecordingGroups": "रेकॉर्डिंग गट", - "HeaderCameraUploads": "कॅमेरा अपलोड", "CameraImageUploadedFrom": "एक नवीन कॅमेरा चित्र {0} येथून अपलोड केले आहे", "Application": "अॅप्लिकेशन", "AppDeviceValues": "अॅप: {0}, यंत्र: {1}", diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json index 7f8df1289..5e3d095ff 100644 --- a/Emby.Server.Implementations/Localization/Core/ms.json +++ b/Emby.Server.Implementations/Localization/Core/ms.json @@ -16,7 +16,6 @@ "Folders": "Fail-fail", "Genres": "Genre-genre", "HeaderAlbumArtists": "Album Artis-artis", - "HeaderCameraUploads": "Muatnaik Kamera", "HeaderContinueWatching": "Terus Menonton", "HeaderFavoriteAlbums": "Album-album Kegemaran", "HeaderFavoriteArtists": "Artis-artis Kegemaran", diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 1b55c2e38..245c3cd63 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -16,7 +16,6 @@ "Folders": "Mapper", "Genres": "Sjangre", "HeaderAlbumArtists": "Albumartister", - "HeaderCameraUploads": "Kameraopplastinger", "HeaderContinueWatching": "Fortsett å se", "HeaderFavoriteAlbums": "Favorittalbum", "HeaderFavoriteArtists": "Favorittartister", @@ -45,12 +44,12 @@ "NameSeasonNumber": "Sesong {0}", "NameSeasonUnknown": "Sesong ukjent", "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", - "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig", + "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp", - "NotificationOptionInstallationFailed": "Installasjonsfeil", + "NotificationOptionInstallationFailed": "Installasjonen feilet", "NotificationOptionNewLibraryContent": "Nytt innhold lagt til", "NotificationOptionPluginError": "Pluginfeil", "NotificationOptionPluginInstalled": "Plugin installert", @@ -71,7 +70,7 @@ "ScheduledTaskFailedWithName": "{0} mislykkes", "ScheduledTaskStartedWithName": "{0} startet", "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", - "Shows": "Programmer", + "Shows": "Program", "Songs": "Sanger", "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", @@ -88,7 +87,7 @@ "UserOnlineFromDevice": "{0} er tilkoblet fra {1}", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}", - "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}", + "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}", "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", "ValueSpecialEpisodeName": "Spesialepisode - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/ne.json b/Emby.Server.Implementations/Localization/Core/ne.json index 38c073709..8e820d40c 100644 --- a/Emby.Server.Implementations/Localization/Core/ne.json +++ b/Emby.Server.Implementations/Localization/Core/ne.json @@ -41,7 +41,6 @@ "HeaderFavoriteArtists": "मनपर्ने कलाकारहरू", "HeaderFavoriteAlbums": "मनपर्ने एल्बमहरू", "HeaderContinueWatching": "हेर्न जारी राख्नुहोस्", - "HeaderCameraUploads": "क्यामेरा अपलोडहरू", "HeaderAlbumArtists": "एल्बमका कलाकारहरू", "Genres": "विधाहरू", "Folders": "फोल्डरहरू", diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json index 41c74d54d..e102b92b9 100644 --- a/Emby.Server.Implementations/Localization/Core/nl.json +++ b/Emby.Server.Implementations/Localization/Core/nl.json @@ -16,7 +16,6 @@ "Folders": "Mappen", "Genres": "Genres", "HeaderAlbumArtists": "Albumartiesten", - "HeaderCameraUploads": "Camera-uploads", "HeaderContinueWatching": "Kijken hervatten", "HeaderFavoriteAlbums": "Favoriete albums", "HeaderFavoriteArtists": "Favoriete artiesten", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 281cadac5..6236515b2 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -19,7 +19,6 @@ "HeaderFavoriteArtists": "Favoritt Artistar", "HeaderFavoriteAlbums": "Favoritt Album", "HeaderContinueWatching": "Fortsett å sjå", - "HeaderCameraUploads": "Kamera Opplastingar", "HeaderAlbumArtists": "Album Artist", "Genres": "Sjangrar", "Folders": "Mapper", @@ -35,7 +34,7 @@ "AuthenticationSucceededWithUserName": "{0} Har logga inn", "Artists": "Artistar", "Application": "Program", - "AppDeviceValues": "App: {0}, Einheit: {1}", + "AppDeviceValues": "App: {0}, Eining: {1}", "Albums": "Album", "NotificationOptionServerRestartRequired": "Tenaren krev omstart", "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert", @@ -43,7 +42,7 @@ "NotificationOptionPluginInstalled": "Tilleggsprogram installert", "NotificationOptionPluginError": "Tilleggsprogram feila", "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til", - "NotificationOptionInstallationFailed": "Installasjonen feila", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa", "NotificationOptionAudioPlayback": "Lydavspilling påbyrja", @@ -56,5 +55,62 @@ "MusicVideos": "Musikkvideoar", "Music": "Musikk", "Movies": "Filmar", - "MixedContent": "Blanda innhald" + "MixedContent": "Blanda innhald", + "Sync": "Synkronisera", + "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.", + "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar", + "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.", + "TaskRefreshChannels": "Oppdater kanalar", + "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.", + "TaskCleanTranscode": "Reins transkodemappe", + "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.", + "TaskUpdatePlugins": "Oppdaterer programtillegg", + "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.", + "TaskRefreshPeople": "Oppdater personar", + "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.", + "TaskCleanLogs": "Reins loggmappe", + "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.", + "TaskRefreshLibrary": "Skann mediebibliotek", + "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.", + "TaskRefreshChapterImages": "Trekk ut kapittelbilete", + "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.", + "TaskCleanCache": "Rens mappe for hurtiglager", + "TasksChannelsCategory": "Internettkanalar", + "TasksApplicationCategory": "Applikasjon", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Vedlikehald", + "VersionNumber": "Versjon {0}", + "ValueSpecialEpisodeName": "Spesialepisode - {0}", + "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", + "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}", + "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}", + "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}", + "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", + "UserOnlineFromDevice": "{0} er direktekopla frå {1}", + "UserOfflineFromDevice": "{0} har kopla frå {1}", + "UserLockedOutWithName": "Brukar {0} har blitt utestengd", + "UserDownloadingItemWithValues": "{0} lastar ned {1}", + "UserDeletedWithName": "Brukar {0} er sletta", + "UserCreatedWithName": "Brukar {0} er oppretta", + "User": "Brukar", + "TvShows": "TV-seriar", + "System": "System", + "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}", + "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.", + "Songs": "Songar", + "Shows": "Program", + "ServerNameNeedsToBeRestarted": "{0} må omstartast", + "ScheduledTaskStartedWithName": "{0} starta", + "ScheduledTaskFailedWithName": "{0} feila", + "ProviderValue": "Leverandør: {0}", + "PluginUpdatedWithName": "{0} blei oppdatert", + "PluginUninstalledWithName": "{0} blei avinstallert", + "PluginInstalledWithName": "{0} blei installert", + "Plugin": "Programtillegg", + "Playlists": "Speleliste", + "Photos": "Foto", + "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa", + "NotificationOptionVideoPlayback": "Videoavspeling starta", + "NotificationOptionUserLockedOut": "Brukar er utestengd", + "NotificationOptionTaskFailed": "Planlagt oppgåve feila" } diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json index bdc0d0169..003e591b3 100644 --- a/Emby.Server.Implementations/Localization/Core/pl.json +++ b/Emby.Server.Implementations/Localization/Core/pl.json @@ -16,7 +16,6 @@ "Folders": "Foldery", "Genres": "Gatunki", "HeaderAlbumArtists": "Wykonawcy albumów", - "HeaderCameraUploads": "Przekazane obrazy", "HeaderContinueWatching": "Kontynuuj odtwarzanie", "HeaderFavoriteAlbums": "Ulubione albumy", "HeaderFavoriteArtists": "Ulubieni wykonawcy", diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json index 275195640..5e49ca702 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-BR.json +++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json @@ -16,7 +16,6 @@ "Folders": "Pastas", "Genres": "Gêneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Envios da Câmera", "HeaderContinueWatching": "Continuar Assistindo", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas favoritos", diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index c1fb65743..90a4941c5 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -16,7 +16,6 @@ "Folders": "Pastas", "Genres": "Géneros", "HeaderAlbumArtists": "Artistas do Álbum", - "HeaderCameraUploads": "Envios a partir da câmara", "HeaderContinueWatching": "Continuar a Ver", "HeaderFavoriteAlbums": "Álbuns Favoritos", "HeaderFavoriteArtists": "Artistas Favoritos", @@ -26,7 +25,7 @@ "HeaderLiveTV": "TV em Direto", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", - "HomeVideos": "Videos caseiros", + "HomeVideos": "Vídeos Caseiros", "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json index b534d0bbe..2079940cd 100644 --- a/Emby.Server.Implementations/Localization/Core/pt.json +++ b/Emby.Server.Implementations/Localization/Core/pt.json @@ -83,7 +83,6 @@ "Playlists": "Listas de Reprodução", "Photos": "Fotografias", "Movies": "Filmes", - "HeaderCameraUploads": "Carregamentos a partir da câmara", "FailedLoginAttemptWithUserName": "Tentativa de ligação falhada a partir de {0}", "DeviceOnlineWithName": "{0} está connectado", "DeviceOfflineWithName": "{0} desconectou-se", diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index 699dd26da..bc008df3b 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -74,7 +74,6 @@ "HeaderFavoriteArtists": "Artiști Favoriți", "HeaderFavoriteAlbums": "Albume Favorite", "HeaderContinueWatching": "Vizionează în continuare", - "HeaderCameraUploads": "Incărcări Cameră Foto", "HeaderAlbumArtists": "Album Artiști", "Genres": "Genuri", "Folders": "Dosare", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 71ee6446c..c0db2cf7f 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -16,12 +16,11 @@ "Folders": "Папки", "Genres": "Жанры", "HeaderAlbumArtists": "Исполнители альбома", - "HeaderCameraUploads": "Камеры", "HeaderContinueWatching": "Продолжение просмотра", "HeaderFavoriteAlbums": "Избранные альбомы", "HeaderFavoriteArtists": "Избранные исполнители", "HeaderFavoriteEpisodes": "Избранные эпизоды", - "HeaderFavoriteShows": "Избранные передачи", + "HeaderFavoriteShows": "Избранные сериалы", "HeaderFavoriteSongs": "Избранные композиции", "HeaderLiveTV": "Эфир", "HeaderNextUp": "Очередное", @@ -114,5 +113,7 @@ "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).", "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.", "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.", - "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе." + "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе.", + "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.", + "TaskCleanActivityLog": "Очистить журнал активности" } diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 0ee652637..8e5026944 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -16,7 +16,6 @@ "Folders": "Priečinky", "Genres": "Žánre", "HeaderAlbumArtists": "Umelci albumu", - "HeaderCameraUploads": "Nahrané fotografie", "HeaderContinueWatching": "Pokračovať v pozeraní", "HeaderFavoriteAlbums": "Obľúbené albumy", "HeaderFavoriteArtists": "Obľúbení umelci", diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json index 329c562e7..66681f025 100644 --- a/Emby.Server.Implementations/Localization/Core/sl-SI.json +++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json @@ -3,21 +3,20 @@ "AppDeviceValues": "Aplikacija: {0}, Naprava: {1}", "Application": "Aplikacija", "Artists": "Izvajalci", - "AuthenticationSucceededWithUserName": "{0} preverjanje pristnosti uspešno", + "AuthenticationSucceededWithUserName": "{0} se je uspešno prijavil", "Books": "Knjige", - "CameraImageUploadedFrom": "Nova fotografija je bila naložena z {0}", + "CameraImageUploadedFrom": "Nova fotografija je bila naložena iz {0}", "Channels": "Kanali", "ChapterNameValue": "Poglavje {0}", "Collections": "Zbirke", "DeviceOfflineWithName": "{0} je prekinil povezavo", "DeviceOnlineWithName": "{0} je povezan", - "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}", + "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}", "Favorites": "Priljubljeno", "Folders": "Mape", "Genres": "Zvrsti", "HeaderAlbumArtists": "Izvajalci albuma", - "HeaderCameraUploads": "Posnetki kamere", - "HeaderContinueWatching": "Nadaljuj gledanje", + "HeaderContinueWatching": "Nadaljuj z ogledom", "HeaderFavoriteAlbums": "Priljubljeni albumi", "HeaderFavoriteArtists": "Priljubljeni izvajalci", "HeaderFavoriteEpisodes": "Priljubljene epizode", @@ -33,23 +32,23 @@ "LabelIpAddressValue": "IP naslov: {0}", "LabelRunningTimeValue": "Čas trajanja: {0}", "Latest": "Najnovejše", - "MessageApplicationUpdated": "Jellyfin Server je bil posodobljen", - "MessageApplicationUpdatedTo": "Jellyfin Server je bil posodobljen na {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitve strežnika {0} je bil posodobljen", + "MessageApplicationUpdated": "Jellyfin strežnik je bil posodobljen", + "MessageApplicationUpdatedTo": "Jellyfin strežnik je bil posodobljen na {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Oddelek nastavitev {0} je bil posodobljen", "MessageServerConfigurationUpdated": "Nastavitve strežnika so bile posodobljene", - "MixedContent": "Razne vsebine", + "MixedContent": "Mešane vsebine", "Movies": "Filmi", "Music": "Glasba", "MusicVideos": "Glasbeni videi", "NameInstallFailed": "{0} namestitev neuspešna", "NameSeasonNumber": "Sezona {0}", - "NameSeasonUnknown": "Season neznana", + "NameSeasonUnknown": "Neznana sezona", "NewVersionIsAvailable": "Nova različica Jellyfin strežnika je na voljo za prenos.", "NotificationOptionApplicationUpdateAvailable": "Posodobitev aplikacije je na voljo", "NotificationOptionApplicationUpdateInstalled": "Posodobitev aplikacije je bila nameščena", - "NotificationOptionAudioPlayback": "Predvajanje zvoka začeto", - "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka zaustavljeno", - "NotificationOptionCameraImageUploaded": "Posnetek kamere naložen", + "NotificationOptionAudioPlayback": "Predvajanje zvoka se je začelo", + "NotificationOptionAudioPlaybackStopped": "Predvajanje zvoka se je ustavilo", + "NotificationOptionCameraImageUploaded": "Fotografija naložena", "NotificationOptionInstallationFailed": "Namestitev neuspešna", "NotificationOptionNewLibraryContent": "Nove vsebine dodane", "NotificationOptionPluginError": "Napaka dodatka", @@ -57,41 +56,41 @@ "NotificationOptionPluginUninstalled": "Dodatek odstranjen", "NotificationOptionPluginUpdateInstalled": "Posodobitev dodatka nameščena", "NotificationOptionServerRestartRequired": "Potreben je ponovni zagon strežnika", - "NotificationOptionTaskFailed": "Razporejena naloga neuspešna", + "NotificationOptionTaskFailed": "Načrtovano opravilo neuspešno", "NotificationOptionUserLockedOut": "Uporabnik zaklenjen", "NotificationOptionVideoPlayback": "Predvajanje videa se je začelo", "NotificationOptionVideoPlaybackStopped": "Predvajanje videa se je ustavilo", "Photos": "Fotografije", "Playlists": "Seznami predvajanja", - "Plugin": "Plugin", + "Plugin": "Dodatek", "PluginInstalledWithName": "{0} je bil nameščen", "PluginUninstalledWithName": "{0} je bil odstranjen", "PluginUpdatedWithName": "{0} je bil posodobljen", - "ProviderValue": "Provider: {0}", + "ProviderValue": "Ponudnik: {0}", "ScheduledTaskFailedWithName": "{0} ni uspelo", "ScheduledTaskStartedWithName": "{0} začeto", "ServerNameNeedsToBeRestarted": "{0} mora biti ponovno zagnan", "Shows": "Serije", "Songs": "Pesmi", - "StartupEmbyServerIsLoading": "Jellyfin Server se nalaga. Poskusi ponovno kasneje.", + "StartupEmbyServerIsLoading": "Jellyfin strežnik se zaganja. Poskusite ponovno kasneje.", "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}", "SubtitleDownloadFailureFromForItem": "Neuspešen prenos podnapisov iz {0} za {1}", "Sync": "Sinhroniziraj", - "System": "System", + "System": "Sistem", "TvShows": "TV serije", - "User": "User", + "User": "Uporabnik", "UserCreatedWithName": "Uporabnik {0} je bil ustvarjen", "UserDeletedWithName": "Uporabnik {0} je bil izbrisan", "UserDownloadingItemWithValues": "{0} prenaša {1}", "UserLockedOutWithName": "Uporabnik {0} je bil zaklenjen", "UserOfflineFromDevice": "{0} je prekinil povezavo z {1}", - "UserOnlineFromDevice": "{0} je aktiven iz {1}", + "UserOnlineFromDevice": "{0} je aktiven na {1}", "UserPasswordChangedWithName": "Geslo za uporabnika {0} je bilo spremenjeno", "UserPolicyUpdatedWithName": "Pravilnik uporabe je bil posodobljen za uporabnika {0}", "UserStartedPlayingItemWithValues": "{0} predvaja {1} na {2}", "UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}", "ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici", - "ValueSpecialEpisodeName": "Poseben - {0}", + "ValueSpecialEpisodeName": "Posebna - {0}", "VersionNumber": "Različica {0}", "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise", "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.", @@ -103,7 +102,7 @@ "TaskRefreshPeopleDescription": "Osveži metapodatke za igralce in režiserje v vaši knjižnici.", "TaskRefreshPeople": "Osveži osebe", "TaskCleanLogsDescription": "Izbriše dnevniške datoteke starejše od {0} dni.", - "TaskCleanLogs": "Počisti mapo dnevnika", + "TaskCleanLogs": "Počisti mapo dnevnikov", "TaskRefreshLibraryDescription": "Preišče vašo knjižnico za nove datoteke in osveži metapodatke.", "TaskRefreshLibrary": "Preišči knjižnico predstavnosti", "TaskRefreshChapterImagesDescription": "Ustvari sličice za poglavja videoposnetkov.", diff --git a/Emby.Server.Implementations/Localization/Core/sq.json b/Emby.Server.Implementations/Localization/Core/sq.json new file mode 100644 index 000000000..0d909b06e --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/sq.json @@ -0,0 +1,116 @@ +{ + "MessageApplicationUpdatedTo": "Serveri Jellyfin u përditesua në versionin {0}", + "Inherit": "Trashgimi", + "TaskDownloadMissingSubtitlesDescription": "Kërkon në internet për titra që mungojnë bazuar tek konfigurimi i metadata-ve.", + "TaskDownloadMissingSubtitles": "Shkarko titra që mungojnë", + "TaskRefreshChannelsDescription": "Rifreskon informacionin e kanaleve të internetit.", + "TaskRefreshChannels": "Rifresko Kanalet", + "TaskCleanTranscodeDescription": "Fshin skedarët e transkodimit që janë më të vjetër se një ditë.", + "TaskCleanTranscode": "Fshi dosjen e transkodimit", + "TaskUpdatePluginsDescription": "Shkarkon dhe instalon përditësimi për plugin që janë konfiguruar të përditësohen automatikisht.", + "TaskUpdatePlugins": "Përditëso Plugin", + "TaskRefreshPeopleDescription": "Përditëson metadata të aktorëve dhe regjizorëve në librarinë tuaj.", + "TaskRefreshPeople": "Rifresko aktorët", + "TaskCleanLogsDescription": "Fshin skëdarët log që janë më të vjetër se {0} ditë.", + "TaskCleanLogs": "Fshi dosjen Log", + "TaskRefreshLibraryDescription": "Skanon librarinë media për skedarë të rinj dhe rifreskon metadata.", + "TaskRefreshLibrary": "Skano librarinë media", + "TaskRefreshChapterImagesDescription": "Krijon imazh për videot që kanë kapituj.", + "TaskRefreshChapterImages": "Ekstrakto Imazhet e Kapitullit", + "TaskCleanCacheDescription": "Fshi skedarët e cache-s që nuk i duhen më sistemit.", + "TaskCleanCache": "Pastro memorjen cache", + "TasksChannelsCategory": "Kanalet nga interneti", + "TasksApplicationCategory": "Aplikacioni", + "TasksLibraryCategory": "Libraria", + "TasksMaintenanceCategory": "Mirëmbajtje", + "VersionNumber": "Versioni {0}", + "ValueSpecialEpisodeName": "Speciale - {0}", + "ValueHasBeenAddedToLibrary": "{0} u shtua tek libraria juaj", + "UserStoppedPlayingItemWithValues": "{0} mbaroi së shikuari {1} tek {2}", + "UserStartedPlayingItemWithValues": "{0} po shikon {1} tek {2}", + "UserPolicyUpdatedWithName": "Politika e përdoruesit u përditësua për {0}", + "UserPasswordChangedWithName": "Fjalëkalimi u ndryshua për përdoruesin {0}", + "UserOnlineFromDevice": "{0} është në linjë nga {1}", + "UserOfflineFromDevice": "{0} u shkëput nga {1}", + "UserLockedOutWithName": "Përdoruesi {0} u përjashtua", + "UserDownloadingItemWithValues": "{0} po shkarkon {1}", + "UserDeletedWithName": "Përdoruesi {0} u fshi", + "UserCreatedWithName": "Përdoruesi {0} u krijua", + "User": "Përdoruesi", + "TvShows": "Seriale TV", + "System": "Sistemi", + "Sync": "Sinkronizo", + "SubtitleDownloadFailureFromForItem": "Titrat deshtuan të shkarkohen nga {0} për {1}", + "StartupEmbyServerIsLoading": "Serveri Jellyfin po ngarkohet. Ju lutemi provoni përseri pas pak.", + "Songs": "Këngë", + "Shows": "Seriale", + "ServerNameNeedsToBeRestarted": "{0} duhet të ristartoj", + "ScheduledTaskStartedWithName": "{0} filloi", + "ScheduledTaskFailedWithName": "{0} dështoi", + "ProviderValue": "Ofruesi: {0}", + "PluginUpdatedWithName": "{0} u përditësua", + "PluginUninstalledWithName": "{0} u çinstalua", + "PluginInstalledWithName": "{0} u instalua", + "Plugin": "Plugin", + "Playlists": "Listat për luajtje", + "Photos": "Fotografitë", + "NotificationOptionVideoPlaybackStopped": "Luajtja e videos ndaloi", + "NotificationOptionVideoPlayback": "Luajtja e videos filloi", + "NotificationOptionUserLockedOut": "Përdoruesi u përjashtua", + "NotificationOptionTaskFailed": "Ushtrimi i planifikuar dështoi", + "NotificationOptionServerRestartRequired": "Kërkohet ristartim i serverit", + "NotificationOptionPluginUpdateInstalled": "Përditësimi i plugin u instalua", + "NotificationOptionPluginUninstalled": "Plugin u çinstalua", + "NotificationOptionPluginInstalled": "Plugin u instalua", + "NotificationOptionPluginError": "Plugin dështoi", + "NotificationOptionNewLibraryContent": "Një përmbajtje e re u shtua", + "NotificationOptionInstallationFailed": "Instalimi dështoi", + "NotificationOptionCameraImageUploaded": "Fotoja nga kamera u ngarkua", + "NotificationOptionAudioPlaybackStopped": "Luajtja e audios ndaloi", + "NotificationOptionAudioPlayback": "Luajtja e audios filloi", + "NotificationOptionApplicationUpdateInstalled": "Përditësimi i aplikacionit u instalua", + "NotificationOptionApplicationUpdateAvailable": "Një perditësim i aplikacionit është gati", + "NewVersionIsAvailable": "Një version i ri i Jellyfin është gati për tu shkarkuar.", + "NameSeasonUnknown": "Sezon i panjohur", + "NameSeasonNumber": "Sezoni {0}", + "NameInstallFailed": "Instalimi i {0} dështoi", + "MusicVideos": "Video muzikore", + "Music": "Muzikë", + "Movies": "Filma", + "MixedContent": "Përmbajtje e përzier", + "MessageServerConfigurationUpdated": "Konfigurimet e serverit u përditësuan", + "MessageNamedServerConfigurationUpdatedWithValue": "Seksioni i konfigurimit të serverit {0} u përditësua", + "MessageApplicationUpdated": "Serveri Jellyfin u përditësua", + "Latest": "Të fundit", + "LabelRunningTimeValue": "Kohëzgjatja: {0}", + "LabelIpAddressValue": "Adresa IP: {0}", + "ItemRemovedWithName": "{0} u fshi nga libraria", + "ItemAddedWithName": "{0} u shtua tek libraria", + "HomeVideos": "Video personale", + "HeaderRecordingGroups": "Grupet e regjistrimit", + "HeaderNextUp": "Në vazhdim", + "HeaderLiveTV": "TV Live", + "HeaderFavoriteSongs": "Kënget e preferuara", + "HeaderFavoriteShows": "Serialet e preferuar", + "HeaderFavoriteEpisodes": "Episodet e preferuar", + "HeaderFavoriteArtists": "Artistët e preferuar", + "HeaderFavoriteAlbums": "Albumet e preferuar", + "HeaderContinueWatching": "Vazhdo të shikosh", + "HeaderAlbumArtists": "Artistët e albumeve", + "Genres": "Zhanre", + "Folders": "Dosje", + "Favorites": "Të preferuara", + "FailedLoginAttemptWithUserName": "Përpjekja për hyrje dështoi nga {0}", + "DeviceOnlineWithName": "{0} u lidh", + "DeviceOfflineWithName": "{0} u shkëput", + "Collections": "Koleksione", + "ChapterNameValue": "Kapituj", + "Channels": "Kanale", + "CameraImageUploadedFrom": "Një foto e re nga kamera u ngarkua nga {0}", + "Books": "Libra", + "AuthenticationSucceededWithUserName": "{0} u identifikua me sukses", + "Artists": "Artistë", + "Application": "Aplikacioni", + "AppDeviceValues": "Aplikacioni: {0}, Pajisja: {1}", + "Albums": "Albume" +} diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json index 5f3cbb1c8..2b1eccfaf 100644 --- a/Emby.Server.Implementations/Localization/Core/sr.json +++ b/Emby.Server.Implementations/Localization/Core/sr.json @@ -74,7 +74,6 @@ "HeaderFavoriteArtists": "Омиљени извођачи", "HeaderFavoriteAlbums": "Омиљени албуми", "HeaderContinueWatching": "Настави гледање", - "HeaderCameraUploads": "Слања са камере", "HeaderAlbumArtists": "Извођачи албума", "Genres": "Жанрови", "Folders": "Фасцикле", diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index c8662b2ca..bea294ba2 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -9,14 +9,13 @@ "Channels": "Kanaler", "ChapterNameValue": "Kapitel {0}", "Collections": "Samlingar", - "DeviceOfflineWithName": "{0} har kopplat från", + "DeviceOfflineWithName": "{0} har kopplat ner", "DeviceOnlineWithName": "{0} är ansluten", "FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}", "Favorites": "Favoriter", "Folders": "Mappar", "Genres": "Genrer", "HeaderAlbumArtists": "Albumartister", - "HeaderCameraUploads": "Kamerauppladdningar", "HeaderContinueWatching": "Fortsätt kolla", "HeaderFavoriteAlbums": "Favoritalbum", "HeaderFavoriteArtists": "Favoritartister", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index d6be86da3..8089fc304 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -18,20 +18,19 @@ "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன", "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது", "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது", - "Inherit": "மரபரிமையாகப் பெறு", + "Inherit": "மரபுரிமையாகப் பெறு", "HeaderRecordingGroups": "பதிவு குழுக்கள்", - "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்", "Folders": "கோப்புறைகள்", "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", "Collections": "தொகுப்புகள்", - "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது", + "CameraImageUploadedFrom": "{0} இல் இருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது", "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}", "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு", "TaskRefreshChannels": "சேனல்களை புதுப்பி", "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி", - "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்", + "TaskRefreshLibrary": "ஊடக நூலகத்தை ஆராய்", "TasksChannelsCategory": "இணைய சேனல்கள்", "TasksApplicationCategory": "செயலி", "TasksLibraryCategory": "நூலகம்", @@ -46,7 +45,7 @@ "Sync": "ஒத்திசைவு", "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.", "Songs": "பாடல்கள்", - "Shows": "தொடர்கள்", + "Shows": "நிகழ்ச்சிகள்", "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்", "ScheduledTaskStartedWithName": "{0} துவங்கியது", "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது", @@ -67,20 +66,20 @@ "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது", "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது", "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்", - "NameSeasonUnknown": "பருவம் அறியப்படாதவை", + "NameSeasonUnknown": "அறியப்படாத பருவம்", "NameSeasonNumber": "பருவம் {0}", "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது", "MusicVideos": "இசைப்படங்கள்", "Music": "இசை", "Movies": "திரைப்படங்கள்", - "Latest": "புதியன", + "Latest": "புதியவை", "LabelRunningTimeValue": "ஓடும் நேரம்: {0}", "LabelIpAddressValue": "ஐபி முகவரி: {0}", "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது", "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது", - "HeaderNextUp": "அடுத்ததாக", + "HeaderNextUp": "அடுத்தது", "HeaderLiveTV": "நேரடித் தொலைக்காட்சி", - "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்", + "HeaderFavoriteSongs": "பிடித்த பாடல்கள்", "HeaderFavoriteShows": "பிடித்த தொடர்கள்", "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்", "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", @@ -93,25 +92,25 @@ "Channels": "சேனல்கள்", "Books": "புத்தகங்கள்", "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது", - "Artists": "கலைஞர்", + "Artists": "கலைஞர்கள்", "Application": "செயலி", "Albums": "ஆல்பங்கள்", "NewVersionIsAvailable": "ஜெல்லிஃபின் சேவையகத்தின் புதிய பதிப்பு பதிவிறக்கத்திற்கு கிடைக்கிறது.", - "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0 புதுப்பிக்கப்பட்டது", + "MessageNamedServerConfigurationUpdatedWithValue": "சேவையக உள்ளமைவு பிரிவு {0} புதுப்பிக்கப்பட்டது", "TaskCleanCacheDescription": "கணினிக்கு இனி தேவைப்படாத தற்காலிக கோப்புகளை நீக்கு.", "UserOfflineFromDevice": "{0} இலிருந்து {1} துண்டிக்கப்பட்டுள்ளது", - "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0 } இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", - "TaskDownloadMissingSubtitlesDescription": "மெட்டாடேட்டா உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", - "TaskCleanTranscodeDescription": "டிரான்ஸ்கோட் கோப்புகளை ஒரு நாளுக்கு மேல் பழையதாக நீக்குகிறது.", - "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட செருகுநிரல்களுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", - "TaskRefreshPeopleDescription": "உங்கள் மீடியா நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மெட்டாடேட்டாவை புதுப்பிக்கும்.", + "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இலிருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன", + "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.", + "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.", + "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.", + "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.", "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.", - "TaskCleanLogs": "பதிவு அடைவு சுத்தம் செய்யுங்கள்", - "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் மீடியா நூலகத்தை ஸ்கேன் செய்து மீத்தரவை புதுப்பிக்கும்.", + "TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்", + "TaskRefreshLibraryDescription": "புதிய கோப்புகளுக்காக உங்கள் ஊடக நூலகத்தை ஆராய்ந்து மீத்தரவை புதுப்பிக்கும்.", "TaskRefreshChapterImagesDescription": "அத்தியாயங்களைக் கொண்ட வீடியோக்களுக்கான சிறு உருவங்களை உருவாக்குகிறது.", "ValueHasBeenAddedToLibrary": "உங்கள் மீடியா நூலகத்தில் {0} சேர்க்கப்பட்டது", "UserOnlineFromDevice": "{1} இருந்து {0} ஆன்லைன்", "HomeVideos": "முகப்பு வீடியோக்கள்", - "UserStoppedPlayingItemWithValues": "{2} இல் {1} முடித்துவிட்டது", + "UserStoppedPlayingItemWithValues": "{0} {2} இல் {1} முடித்துவிட்டது", "UserStartedPlayingItemWithValues": "{0} {2}இல் {1} ஐ இயக்குகிறது" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 576aaeb1b..71dd2c7a3 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -1,73 +1,116 @@ { "ProviderValue": "ผู้ให้บริการ: {0}", - "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว", - "PluginUninstalledWithName": "ถอนการติดตั้ง {0}", - "PluginInstalledWithName": "{0} ได้รับการติดตั้ง", - "Plugin": "Plugin", - "Playlists": "รายการ", + "PluginUpdatedWithName": "อัปเดต {0} แล้ว", + "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว", + "PluginInstalledWithName": "ติดตั้ง {0} แล้ว", + "Plugin": "ปลั๊กอิน", + "Playlists": "เพลย์ลิสต์", "Photos": "รูปภาพ", - "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video", - "NotificationOptionVideoPlayback": "เริ่มแสดง Video", - "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out", - "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว", - "NotificationOptionServerRestartRequired": "ควร Restart Server", - "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว", - "NotificationOptionPluginUninstalled": "ถอด Plugin", - "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว", - "NotificationOptionPluginError": "Plugin ล้มเหลว", - "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว", - "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว", - "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload", - "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง", + "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ", + "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ", + "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก", + "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว", + "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์", + "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว", + "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว", + "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว", + "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว", + "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว", + "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง", "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง", - "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว", - "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว", - "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่", - "NameSeasonUnknown": "ไม่ทราบปี", - "NameSeasonNumber": "ปี {0}", - "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ", - "MusicVideos": "MV", - "Music": "เพลง", - "Movies": "ภาพยนต์", - "MixedContent": "รายการแบบผสม", - "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว", - "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว", - "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}", - "MessageApplicationUpdated": "Jellyfin Server update แล้ว", + "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอปพลิเคชันแล้ว", + "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอปพลิเคชัน", + "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว", + "NameSeasonUnknown": "ไม่ทราบซีซัน", + "NameSeasonNumber": "ซีซัน {0}", + "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว", + "MusicVideos": "มิวสิควิดีโอ", + "Music": "ดนตรี", + "Movies": "ภาพยนตร์", + "MixedContent": "เนื้อหาผสม", + "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว", + "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว", + "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}", + "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว", "Latest": "ล่าสุด", - "LabelRunningTimeValue": "เวลาที่เล่น : {0}", - "LabelIpAddressValue": "IP address: {0}", - "ItemRemovedWithName": "{0} ถูกลบจากรายการ", - "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ", - "Inherit": "การสืบทอด", - "HomeVideos": "วีดีโอส่วนตัว", - "HeaderRecordingGroups": "ค่ายบันทึก", + "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}", + "LabelIpAddressValue": "ที่อยู่ IP: {0}", + "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี", + "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว", + "Inherit": "สืบทอด", + "HomeVideos": "โฮมวิดีโอ", + "HeaderRecordingGroups": "กลุ่มการบันทึก", "HeaderNextUp": "ถัดไป", - "HeaderLiveTV": "รายการสด", - "HeaderFavoriteSongs": "เพลงโปรด", - "HeaderFavoriteShows": "รายการโชว์โปรด", - "HeaderFavoriteEpisodes": "ฉากโปรด", - "HeaderFavoriteArtists": "นักแสดงโปรด", - "HeaderFavoriteAlbums": "อัมบั้มโปรด", - "HeaderContinueWatching": "ชมต่อจากเดิม", - "HeaderCameraUploads": "Upload รูปภาพ", - "HeaderAlbumArtists": "อัลบั้มนักแสดง", + "HeaderLiveTV": "ทีวีสด", + "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ", + "HeaderFavoriteShows": "รายการที่ชื่นชอบ", + "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ", + "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", + "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", + "HeaderContinueWatching": "ดูต่อ", + "HeaderAlbumArtists": "อัลบั้มศิลปิน", "Genres": "ประเภท", "Folders": "โฟลเดอร์", "Favorites": "รายการโปรด", - "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}", - "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ", - "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ", - "Collections": "ชุด", - "ChapterNameValue": "บทที่ {0}", - "Channels": "ชาแนล", - "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}", + "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}", + "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว", + "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว", + "Collections": "คอลเลกชัน", + "ChapterNameValue": "บท {0}", + "Channels": "ช่อง", + "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}", "Books": "หนังสือ", - "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ", - "Artists": "นักแสดง", - "Application": "แอปพลิเคชั่น", - "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว", + "Artists": "ศิลปิน", + "Application": "แอปพลิเคชัน", + "AppDeviceValues": "แอป: {0}, อุปกรณ์: {1}", "Albums": "อัลบั้ม", "ScheduledTaskStartedWithName": "{0} เริ่มต้น", - "ScheduledTaskFailedWithName": "{0} ล้มเหลว" + "ScheduledTaskFailedWithName": "{0} ล้มเหลว", + "Songs": "เพลง", + "Shows": "รายการ", + "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท", + "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา", + "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป", + "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต", + "TaskRefreshChannels": "รีเฟรชช่อง", + "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน", + "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด", + "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ", + "TaskUpdatePlugins": "อัปเดตปลั๊กอิน", + "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ", + "TaskRefreshPeople": "รีเฟรชบุคคล", + "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน", + "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก", + "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา", + "TaskRefreshLibrary": "สแกนไลบรารีสื่อ", + "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท", + "TaskRefreshChapterImages": "แตกรูปภาพบท", + "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ", + "TaskCleanCache": "ล้างไดเรกทอรีแคช", + "TasksChannelsCategory": "ช่องอินเทอร์เน็ต", + "TasksApplicationCategory": "แอปพลิเคชัน", + "TasksLibraryCategory": "ไลบรารี", + "TasksMaintenanceCategory": "ปิดซ่อมบำรุง", + "VersionNumber": "เวอร์ชัน {0}", + "ValueSpecialEpisodeName": "พิเศษ - {0}", + "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว", + "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}", + "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}", + "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}", + "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}", + "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}", + "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}", + "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก", + "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}", + "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว", + "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว", + "User": "ผู้ใช้งาน", + "TvShows": "รายการทีวี", + "System": "ระบบ", + "Sync": "ซิงค์", + "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", + "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index 3cf3482eb..818b57c7f 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -8,7 +8,7 @@ "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi", "Channels": "Kanallar", "ChapterNameValue": "Bölüm {0}", - "Collections": "Koleksiyonlar", + "Collections": "Koleksiyon", "DeviceOfflineWithName": "{0} bağlantısı kesildi", "DeviceOnlineWithName": "{0} bağlı", "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu", @@ -16,7 +16,6 @@ "Folders": "Klasörler", "Genres": "Türler", "HeaderAlbumArtists": "Albüm Sanatçıları", - "HeaderCameraUploads": "Kamera Yüklemeleri", "HeaderContinueWatching": "İzlemeye Devam Et", "HeaderFavoriteAlbums": "Favori Albümler", "HeaderFavoriteArtists": "Favori Sanatçılar", @@ -24,7 +23,7 @@ "HeaderFavoriteShows": "Favori Diziler", "HeaderFavoriteSongs": "Favori Şarkılar", "HeaderLiveTV": "Canlı TV", - "HeaderNextUp": "Sonraki hafta", + "HeaderNextUp": "Gelecek Hafta", "HeaderRecordingGroups": "Kayıt Grupları", "HomeVideos": "Ev videoları", "Inherit": "Devral", @@ -114,5 +113,6 @@ "TaskRefreshLibrary": "Medya Kütüphanesini Tara", "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.", "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", - "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler." + "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", + "TaskCleanActivityLog": "İşlem Günlüğünü Temizle" } diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index e673465a4..06cc5f633 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -16,7 +16,6 @@ "HeaderFavoriteArtists": "Улюблені виконавці", "HeaderFavoriteAlbums": "Улюблені альбоми", "HeaderContinueWatching": "Продовжити перегляд", - "HeaderCameraUploads": "Завантажено з камери", "HeaderAlbumArtists": "Виконавці альбому", "Genres": "Жанри", "Folders": "Каталоги", diff --git a/Emby.Server.Implementations/Localization/Core/ur_PK.json b/Emby.Server.Implementations/Localization/Core/ur_PK.json index 9a5874e29..fa7b2d4d0 100644 --- a/Emby.Server.Implementations/Localization/Core/ur_PK.json +++ b/Emby.Server.Implementations/Localization/Core/ur_PK.json @@ -105,7 +105,6 @@ "Inherit": "وراثت میں", "HomeVideos": "ہوم ویڈیو", "HeaderRecordingGroups": "ریکارڈنگ گروپس", - "HeaderCameraUploads": "کیمرہ اپلوڈز", "FailedLoginAttemptWithUserName": "لاگن کئ کوشش ناکام {0}", "DeviceOnlineWithName": "{0} متصل ھو چکا ھے", "DeviceOfflineWithName": "{0} منقطع ھو چکا ھے", diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json new file mode 100644 index 000000000..ac74deff8 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -0,0 +1,116 @@ +{ + "Collections": "Bộ Sưu Tập", + "Favorites": "Yêu Thích", + "Folders": "Thư Mục", + "Genres": "Thể Loại", + "HeaderAlbumArtists": "Bộ Sưu Tập Nghệ sĩ", + "HeaderContinueWatching": "Xem Tiếp", + "HeaderLiveTV": "TV Trực Tiếp", + "Movies": "Phim", + "Photos": "Ảnh", + "Playlists": "Danh sách phát", + "Shows": "Chương Trình TV", + "Songs": "Các Bài Hát", + "Sync": "Đồng Bộ", + "ValueSpecialEpisodeName": "Đặc Biệt - {0}", + "Albums": "Albums", + "Artists": "Các Nghệ Sĩ", + "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.", + "TaskDownloadMissingSubtitles": "Tải xuống phụ đề bị thiếu", + "TaskRefreshChannelsDescription": "Làm mới thông tin kênh internet.", + "TaskRefreshChannels": "Làm Mới Kênh", + "TaskCleanTranscodeDescription": "Xóa các tệp chuyển mã cũ hơn một ngày.", + "TaskCleanTranscode": "Làm Sạch Thư Mục Chuyển Mã", + "TaskUpdatePluginsDescription": "Tải xuống và cài đặt các bản cập nhật cho các plugin được định cấu hình để cập nhật tự động.", + "TaskUpdatePlugins": "Cập Nhật Plugins", + "TaskRefreshPeopleDescription": "Cập nhật thông tin chi tiết cho diễn viên và đạo diễn trong thư viện phương tiện của bạn.", + "TaskRefreshPeople": "Làm mới Người dùng", + "TaskCleanLogsDescription": "Xóa tập tin nhật ký cũ hơn {0} ngày.", + "TaskCleanLogs": "Làm sạch nhật ký", + "TaskRefreshLibraryDescription": "Quét thư viện phương tiện của bạn để tìm các tệp mới và làm mới thông tin chi tiết.", + "TaskRefreshLibrary": "Quét Thư viện Phương tiện", + "TaskRefreshChapterImagesDescription": "Tạo hình thu nhỏ cho video có các phân cảnh.", + "TaskRefreshChapterImages": "Trích Xuất Ảnh Phân Cảnh", + "TaskCleanCacheDescription": "Xóa các tệp cache không còn cần thiết của hệ thống.", + "TaskCleanCache": "Làm Sạch Thư Mục Cache", + "TasksChannelsCategory": "Kênh Internet", + "TasksApplicationCategory": "Ứng Dụng", + "TasksLibraryCategory": "Thư Viện", + "TasksMaintenanceCategory": "Bảo Trì", + "VersionNumber": "Phiên Bản {0}", + "ValueHasBeenAddedToLibrary": "{0} đã được thêm vào thư viện của bạn", + "UserStoppedPlayingItemWithValues": "{0} đã phát xong {1} trên {2}", + "UserStartedPlayingItemWithValues": "{0} đang phát {1} trên {2}", + "UserPolicyUpdatedWithName": "Chính sách người dùng đã được cập nhật cho {0}", + "UserPasswordChangedWithName": "Mật khẩu đã được thay đổi cho người dùng {0}", + "UserOnlineFromDevice": "{0} trực tuyến từ {1}", + "UserOfflineFromDevice": "{0} đã ngắt kết nối từ {1}", + "UserLockedOutWithName": "User {0} đã bị khóa", + "UserDownloadingItemWithValues": "{0} đang tải xuống {1}", + "UserDeletedWithName": "Người Dùng {0} đã được xóa", + "UserCreatedWithName": "Người Dùng {0} đã được tạo", + "User": "Người Dùng", + "TvShows": "Chương Trình TV", + "System": "Hệ Thống", + "SubtitleDownloadFailureFromForItem": "Không thể tải xuống phụ đề từ {0} cho {1}", + "StartupEmbyServerIsLoading": "Jellyfin Server đang tải. Vui lòng thử lại trong thời gian ngắn.", + "ServerNameNeedsToBeRestarted": "{0} cần được khởi động lại", + "ScheduledTaskStartedWithName": "{0} đã bắt đầu", + "ScheduledTaskFailedWithName": "{0} đã thất bại", + "ProviderValue": "Provider: {0}", + "PluginUpdatedWithName": "{0} đã cập nhật", + "PluginUninstalledWithName": "{0} đã được gỡ bỏ", + "PluginInstalledWithName": "{0} đã được cài đặt", + "Plugin": "Plugin", + "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng", + "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video", + "NotificationOptionUserLockedOut": "Người dùng bị khóa", + "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch", + "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server", + "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt", + "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin", + "NotificationOptionPluginInstalled": "Đã cài đặt Plugin", + "NotificationOptionPluginError": "Thất bại Plugin", + "NotificationOptionNewLibraryContent": "Nội dung mới được thêm vào", + "NotificationOptionInstallationFailed": "Cài đặt thất bại", + "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh", + "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng", + "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu", + "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt", + "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", + "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", + "NameSeasonUnknown": "Không Rõ Mùa", + "NameSeasonNumber": "Mùa {0}", + "NameInstallFailed": "{0} cài đặt thất bại", + "MusicVideos": "Video Nhạc", + "Music": "Nhạc", + "MixedContent": "Nội dung hỗn hợp", + "MessageServerConfigurationUpdated": "Cấu hình máy chủ đã được cập nhật", + "MessageNamedServerConfigurationUpdatedWithValue": "Phần cấu hình máy chủ {0} đã được cập nhật", + "MessageApplicationUpdatedTo": "Jellyfin Server đã được cập nhật lên {0}", + "MessageApplicationUpdated": "Jellyfin Server đã được cập nhật", + "Latest": "Gần Nhất", + "LabelRunningTimeValue": "Thời Gian Chạy: {0}", + "LabelIpAddressValue": "Địa Chỉ IP: {0}", + "ItemRemovedWithName": "{0} đã xóa khỏi thư viện", + "ItemAddedWithName": "{0} được thêm vào thư viện", + "Inherit": "Thừa hưởng", + "HomeVideos": "Video nhà", + "HeaderRecordingGroups": "Nhóm Ghi Video", + "HeaderNextUp": "Tiếp Theo", + "HeaderFavoriteSongs": "Bài Hát Yêu Thích", + "HeaderFavoriteShows": "Chương Trình Yêu Thích", + "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích", + "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích", + "HeaderFavoriteAlbums": "Album Ưa Thích", + "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}", + "DeviceOnlineWithName": "{0} đã kết nối", + "DeviceOfflineWithName": "{0} đã ngắt kết nối", + "ChapterNameValue": "Phân Cảnh {0}", + "Channels": "Các Kênh", + "CameraImageUploadedFrom": "Một hình ảnh máy ảnh mới đã được tải lên từ {0}", + "Books": "Sách", + "AuthenticationSucceededWithUserName": "{0} xác thực thành công", + "Application": "Ứng Dụng", + "AppDeviceValues": "Ứng Dụng: {0}, Thiết Bị: {1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index 6b563a9b1..e98047a36 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -16,7 +16,6 @@ "Folders": "文件夹", "Genres": "风格", "HeaderAlbumArtists": "专辑作家", - "HeaderCameraUploads": "相机上传", "HeaderContinueWatching": "继续观影", "HeaderFavoriteAlbums": "收藏的专辑", "HeaderFavoriteArtists": "最爱的艺术家", @@ -114,5 +113,6 @@ "TaskCleanCacheDescription": "删除系统不再需要的缓存文件。", "TaskCleanCache": "清理缓存目录", "TasksApplicationCategory": "应用程序", - "TasksMaintenanceCategory": "维护" + "TasksMaintenanceCategory": "维护", + "TaskCleanActivityLog": "清理程序日志" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 1ac62baca..435e294ef 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -16,7 +16,6 @@ "Folders": "檔案夾", "Genres": "風格", "HeaderAlbumArtists": "專輯藝人", - "HeaderCameraUploads": "相機上載", "HeaderContinueWatching": "繼續觀看", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛的藝人", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index a21cdad95..d2e3d77a3 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -1,6 +1,6 @@ { "Albums": "專輯", - "AppDeviceValues": "軟體: {0}, 裝置: {1}", + "AppDeviceValues": "軟體:{0},裝置:{1}", "Application": "應用程式", "Artists": "演出者", "AuthenticationSucceededWithUserName": "{0} 成功授權", @@ -11,12 +11,11 @@ "Collections": "合輯", "DeviceOfflineWithName": "{0} 已經斷線", "DeviceOnlineWithName": "{0} 已經連線", - "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試", + "FailedLoginAttemptWithUserName": "來自使用者 {0} 的失敗登入", "Favorites": "我的最愛", "Folders": "資料夾", "Genres": "風格", "HeaderAlbumArtists": "專輯演出者", - "HeaderCameraUploads": "相機上傳", "HeaderContinueWatching": "繼續觀賞", "HeaderFavoriteAlbums": "最愛專輯", "HeaderFavoriteArtists": "最愛演出者", @@ -28,8 +27,8 @@ "HomeVideos": "自製影片", "ItemAddedWithName": "{0} 已新增至媒體庫", "ItemRemovedWithName": "{0} 已從媒體庫移除", - "LabelIpAddressValue": "IP 位置: {0}", - "LabelRunningTimeValue": "運行時間: {0}", + "LabelIpAddressValue": "IP 位址:{0}", + "LabelRunningTimeValue": "運行時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin Server 已經更新", "MessageApplicationUpdatedTo": "Jellyfin Server 已經更新至 {0}", @@ -42,18 +41,18 @@ "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知季數", - "NewVersionIsAvailable": "新版本的Jellyfin Server 軟體已經推出可供下載。", + "NewVersionIsAvailable": "新版本的 Jellyfin Server 軟體已經可供下載。", "NotificationOptionApplicationUpdateAvailable": "有可用的應用程式更新", - "NotificationOptionApplicationUpdateInstalled": "應用程式已更新", + "NotificationOptionApplicationUpdateInstalled": "軟體更新已安裝", "NotificationOptionAudioPlayback": "音樂開始播放", "NotificationOptionAudioPlaybackStopped": "音樂停止播放", "NotificationOptionCameraImageUploaded": "相機相片已上傳", "NotificationOptionInstallationFailed": "安裝失敗", "NotificationOptionNewLibraryContent": "已新增新內容", - "NotificationOptionPluginError": "插件安裝錯誤", - "NotificationOptionPluginInstalled": "插件已安裝", - "NotificationOptionPluginUninstalled": "插件已移除", - "NotificationOptionPluginUpdateInstalled": "插件已更新", + "NotificationOptionPluginError": "外掛安裝失敗", + "NotificationOptionPluginInstalled": "外掛已安裝", + "NotificationOptionPluginUninstalled": "外掛已移除", + "NotificationOptionPluginUpdateInstalled": "外掛已更新", "NotificationOptionServerRestartRequired": "伺服器需要重新啟動", "NotificationOptionTaskFailed": "排程任務失敗", "NotificationOptionUserLockedOut": "使用者已鎖定", @@ -61,14 +60,14 @@ "NotificationOptionVideoPlaybackStopped": "影片停止播放", "Photos": "相片", "Playlists": "播放清單", - "Plugin": "插件", + "Plugin": "外掛", "PluginInstalledWithName": "{0} 已安裝", "PluginUninstalledWithName": "{0} 已移除", "PluginUpdatedWithName": "{0} 已更新", "ProviderValue": "提供商: {0}", - "ScheduledTaskFailedWithName": "{0} 已失敗", - "ScheduledTaskStartedWithName": "{0} 已開始", - "ServerNameNeedsToBeRestarted": "{0} 需要重新啟動", + "ScheduledTaskFailedWithName": "排程任務 {0} 已失敗", + "ScheduledTaskStartedWithName": "排程任務 {0} 已開始", + "ServerNameNeedsToBeRestarted": "伺服器 {0} 需要重新啟動", "Shows": "節目", "Songs": "歌曲", "StartupEmbyServerIsLoading": "Jellyfin Server正在啟動,請稍後再試一次。", @@ -78,10 +77,10 @@ "User": "使用者", "UserCreatedWithName": "使用者 {0} 已建立", "UserDeletedWithName": "使用者 {0} 已移除", - "UserDownloadingItemWithValues": "{0} 正在下載 {1}", + "UserDownloadingItemWithValues": "使用者 {0} 正在下載 {1}", "UserLockedOutWithName": "使用者 {0} 已鎖定", - "UserOfflineFromDevice": "{0} 已從 {1} 斷線", - "UserOnlineFromDevice": "{0} 已連線,來自 {1}", + "UserOfflineFromDevice": "使用者 {0} 已從 {1} 斷線", + "UserOnlineFromDevice": "使用者 {0} 已從 {1} 連線", "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更", "UserPolicyUpdatedWithName": "使用者條約已更新為 {0}", "UserStartedPlayingItemWithValues": "{0}正在使用 {2} 播放 {1}", @@ -95,23 +94,25 @@ "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。", "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskRefreshChannels": "重新整理頻道", - "TaskUpdatePlugins": "更新插件", - "TaskRefreshPeople": "重新整理人員", - "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。", + "TaskUpdatePlugins": "更新外掛", + "TaskRefreshPeople": "刷新用戶", + "TaskCleanLogsDescription": "刪除超過 {0} 天的舊紀錄檔。", "TaskCleanLogs": "清空紀錄資料夾", - "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。", - "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshLibraryDescription": "重新掃描媒體庫的新檔案並更新描述資料。", + "TaskRefreshLibrary": "重新掃描媒體庫", "TaskRefreshChapterImages": "擷取章節圖片", - "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。", + "TaskCleanCacheDescription": "刪除系統已不需要的快取。", "TaskCleanCache": "清除快取資料夾", "TasksLibraryCategory": "媒體庫", - "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。", + "TaskRefreshChannelsDescription": "重新整理網路頻道資料。", "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", "TaskCleanTranscode": "清除轉碼資料夾", - "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", + "TaskUpdatePluginsDescription": "為設置自動更新的外掛下載並安裝更新。", "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。", - "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。", - "TasksChannelsCategory": "網絡頻道", + "TaskRefreshChapterImagesDescription": "為有章節的影片建立縮圖。", + "TasksChannelsCategory": "網路頻道", "TasksApplicationCategory": "應用程式", - "TasksMaintenanceCategory": "維修" + "TasksMaintenanceCategory": "維護", + "TaskCleanActivityLogDescription": "刪除超過所設時間的活動紀錄。", + "TaskCleanActivityLog": "清除活動紀錄" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 90e2766b8..30aaf3a05 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -413,6 +413,7 @@ namespace Emby.Server.Implementations.Localization yield return new LocalizationOption("Swedish", "sv"); yield return new LocalizationOption("Swiss German", "gsw"); yield return new LocalizationOption("Turkish", "tr"); + yield return new LocalizationOption("Tiếng Việt", "vi"); } } } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 38ceadedb..d3b64fb31 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -152,10 +152,10 @@ namespace Emby.Server.Implementations.Playlists if (options.ItemIdList.Length > 0) { - AddToPlaylistInternal(playlist.Id.ToString("N", CultureInfo.InvariantCulture), options.ItemIdList, user, new DtoOptions(false) + await AddToPlaylistInternal(playlist.Id, options.ItemIdList, user, new DtoOptions(false) { EnableImages = true - }); + }).ConfigureAwait(false); } return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture)); @@ -184,17 +184,17 @@ namespace Emby.Server.Implementations.Playlists return Playlist.GetPlaylistItems(playlistMediaType, items, user, options); } - public void AddToPlaylist(string playlistId, ICollection<Guid> itemIds, Guid userId) + public Task AddToPlaylistAsync(Guid playlistId, ICollection<Guid> itemIds, Guid userId) { var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); - AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) + return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { EnableImages = true }); } - private void AddToPlaylistInternal(string playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) + private async Task AddToPlaylistInternal(Guid playlistId, ICollection<Guid> newItemIds, User user, DtoOptions options) { // Retrieve the existing playlist var playlist = _libraryManager.GetItemById(playlistId) as Playlist @@ -238,7 +238,7 @@ namespace Emby.Server.Implementations.Playlists // Update the playlist in the repository playlist.LinkedChildren = newLinkedChildren; - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); // Update the playlist on disk if (playlist.IsFile) @@ -256,7 +256,7 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public void RemoveFromPlaylist(string playlistId, IEnumerable<string> entryIds) + public async Task RemoveFromPlaylistAsync(string playlistId, IEnumerable<string> entryIds) { if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { @@ -273,7 +273,7 @@ namespace Emby.Server.Implementations.Playlists .Select(i => i.Item1) .ToArray(); - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); if (playlist.IsFile) { @@ -289,7 +289,7 @@ namespace Emby.Server.Implementations.Playlists RefreshPriority.High); } - public void MoveItem(string playlistId, string entryId, int newIndex) + public async Task MoveItemAsync(string playlistId, string entryId, int newIndex) { if (!(_libraryManager.GetItemById(playlistId) is Playlist playlist)) { @@ -322,7 +322,7 @@ namespace Emby.Server.Implementations.Playlists playlist.LinkedChildren = newList.ToArray(); - playlist.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None); + await playlist.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); if (playlist.IsFile) { diff --git a/Emby.Server.Implementations/Plugins/PluginManifest.cs b/Emby.Server.Implementations/Plugins/PluginManifest.cs new file mode 100644 index 000000000..33762791b --- /dev/null +++ b/Emby.Server.Implementations/Plugins/PluginManifest.cs @@ -0,0 +1,60 @@ +using System; + +namespace Emby.Server.Implementations.Plugins +{ + /// <summary> + /// Defines a Plugin manifest file. + /// </summary> + public class PluginManifest + { + /// <summary> + /// Gets or sets the category of the plugin. + /// </summary> + public string Category { get; set; } + + /// <summary> + /// Gets or sets the changelog information. + /// </summary> + public string Changelog { get; set; } + + /// <summary> + /// Gets or sets the description of the plugin. + /// </summary> + public string Description { get; set; } + + /// <summary> + /// Gets or sets the Global Unique Identifier for the plugin. + /// </summary> + public Guid Guid { get; set; } + + /// <summary> + /// Gets or sets the Name of the plugin. + /// </summary> + public string Name { get; set; } + + /// <summary> + /// Gets or sets an overview of the plugin. + /// </summary> + public string Overview { get; set; } + + /// <summary> + /// Gets or sets the owner of the plugin. + /// </summary> + public string Owner { get; set; } + + /// <summary> + /// Gets or sets the compatibility version for the plugin. + /// </summary> + public string TargetAbi { get; set; } + + /// <summary> + /// Gets or sets the timestamp of the plugin. + /// </summary> + public DateTime Timestamp { get; set; } + + /// <summary> + /// Gets or sets the Version number of the plugin. + /// </summary> + public string Version { get; set; } + } +} diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs new file mode 100644 index 000000000..140a67541 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.QuickConnect; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.QuickConnect +{ + /// <summary> + /// Quick connect implementation. + /// </summary> + public class QuickConnectManager : IQuickConnect, IDisposable + { + private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>(); + + private readonly IServerConfigurationManager _config; + private readonly ILogger<QuickConnectManager> _logger; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly IAuthorizationContext _authContext; + private readonly IServerApplicationHost _appHost; + + /// <summary> + /// Initializes a new instance of the <see cref="QuickConnectManager"/> class. + /// Should only be called at server startup when a singleton is created. + /// </summary> + /// <param name="config">Configuration.</param> + /// <param name="logger">Logger.</param> + /// <param name="appHost">Application host.</param> + /// <param name="authContext">Authentication context.</param> + /// <param name="authenticationRepository">Authentication repository.</param> + public QuickConnectManager( + IServerConfigurationManager config, + ILogger<QuickConnectManager> logger, + IServerApplicationHost appHost, + IAuthorizationContext authContext, + IAuthenticationRepository authenticationRepository) + { + _config = config; + _logger = logger; + _appHost = appHost; + _authContext = authContext; + _authenticationRepository = authenticationRepository; + + ReloadConfiguration(); + } + + /// <inheritdoc/> + public int CodeLength { get; set; } = 6; + + /// <inheritdoc/> + public string TokenName { get; set; } = "QuickConnect"; + + /// <inheritdoc/> + public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; + + /// <inheritdoc/> + public int Timeout { get; set; } = 5; + + private DateTime DateActivated { get; set; } + + /// <inheritdoc/> + public void AssertActive() + { + if (State != QuickConnectState.Active) + { + throw new ArgumentException("Quick connect is not active on this server"); + } + } + + /// <inheritdoc/> + public void Activate() + { + DateActivated = DateTime.UtcNow; + SetState(QuickConnectState.Active); + } + + /// <inheritdoc/> + public void SetState(QuickConnectState newState) + { + _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState); + + ExpireRequests(true); + + State = newState; + _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active; + _config.SaveConfiguration(); + + _logger.LogDebug("Configuration saved"); + } + + /// <inheritdoc/> + public QuickConnectResult TryConnect() + { + ExpireRequests(); + + if (State != QuickConnectState.Active) + { + _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State); + throw new AuthenticationException("Quick connect is not active on this server"); + } + + var code = GenerateCode(); + var result = new QuickConnectResult() + { + Secret = GenerateSecureRandom(), + DateAdded = DateTime.UtcNow, + Code = code + }; + + _currentRequests[code] = result; + return result; + } + + /// <inheritdoc/> + public QuickConnectResult CheckRequestStatus(string secret) + { + ExpireRequests(); + AssertActive(); + + string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request with provided secret"); + } + + return result; + } + + /// <inheritdoc/> + public string GenerateCode() + { + Span<byte> raw = stackalloc byte[4]; + + int min = (int)Math.Pow(10, CodeLength - 1); + int max = (int)Math.Pow(10, CodeLength); + + uint scale = uint.MaxValue; + while (scale == uint.MaxValue) + { + _rng.GetBytes(raw); + scale = BitConverter.ToUInt32(raw); + } + + int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue))); + return code.ToString(CultureInfo.InvariantCulture); + } + + /// <inheritdoc/> + public bool AuthorizeRequest(Guid userId, string code) + { + ExpireRequests(); + AssertActive(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request"); + } + + if (result.Authenticated) + { + throw new InvalidOperationException("Request is already authorized"); + } + + result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. + var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout)); + result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1)); + + _authenticationRepository.Create(new AuthenticationInfo + { + AppName = TokenName, + AccessToken = result.Authentication, + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString, + UserId = userId + }); + + _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId); + + return true; + } + + /// <inheritdoc/> + public int DeleteAllDevices(Guid user) + { + var raw = _authenticationRepository.Get(new AuthenticationInfoQuery() + { + DeviceId = _appHost.SystemId, + UserId = user + }); + + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal)); + + var removed = 0; + foreach (var token in tokens) + { + _authenticationRepository.Delete(token); + _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken); + removed++; + } + + return removed; + } + + /// <summary> + /// Dispose. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Dispose. + /// </summary> + /// <param name="disposing">Dispose unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _rng?.Dispose(); + } + } + + private string GenerateSecureRandom(int length = 32) + { + Span<byte> bytes = stackalloc byte[length]; + _rng.GetBytes(bytes); + + return Hex.Encode(bytes); + } + + /// <inheritdoc/> + public void ExpireRequests(bool expireAll = false) + { + // Check if quick connect should be deactivated + if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll) + { + _logger.LogDebug("Quick connect time expired, deactivating"); + SetState(QuickConnectState.Available); + expireAll = true; + } + + // Expire stale connection requests + var code = string.Empty; + var values = _currentRequests.Values.ToList(); + + for (int i = 0; i < values.Count; i++) + { + var added = values[i].DateAdded ?? DateTime.UnixEpoch; + if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll) + { + code = values[i].Code; + _logger.LogDebug("Removing expired request {code}", code); + + if (!_currentRequests.TryRemove(code, out _)) + { + _logger.LogWarning("Request {code} already expired", code); + } + } + } + } + + private void ReloadConfiguration() + { + State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 8a900f42c..56f4133a0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -6,11 +6,10 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -22,37 +21,53 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class ScheduledTaskWorker : IScheduledTaskWorker { - public event EventHandler<GenericEventArgs<double>> TaskProgress; - - /// <summary> - /// Gets the scheduled task. - /// </summary> - /// <value>The scheduled task.</value> - public IScheduledTask ScheduledTask { get; private set; } - /// <summary> /// Gets or sets the json serializer. /// </summary> /// <value>The json serializer.</value> - private IJsonSerializer JsonSerializer { get; set; } + private readonly IJsonSerializer _jsonSerializer; /// <summary> /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - private IApplicationPaths ApplicationPaths { get; set; } + private readonly IApplicationPaths _applicationPaths; /// <summary> - /// Gets the logger. + /// Gets or sets the logger. /// </summary> /// <value>The logger.</value> - private ILogger Logger { get; set; } + private readonly ILogger _logger; /// <summary> - /// Gets the task manager. + /// Gets or sets the task manager. /// </summary> /// <value>The task manager.</value> - private ITaskManager TaskManager { get; set; } + private readonly ITaskManager _taskManager; + + /// <summary> + /// The _last execution result sync lock. + /// </summary> + private readonly object _lastExecutionResultSyncLock = new object(); + + private bool _readFromFile = false; + + /// <summary> + /// The _last execution result. + /// </summary> + private TaskResult _lastExecutionResult; + + private Task _currentTask; + + /// <summary> + /// The _triggers. + /// </summary> + private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers; + + /// <summary> + /// The _id. + /// </summary> + private string _id; /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. @@ -71,7 +86,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// or /// jsonSerializer /// or - /// logger + /// logger. /// </exception> public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger) { @@ -101,23 +116,22 @@ namespace Emby.Server.Implementations.ScheduledTasks } ScheduledTask = scheduledTask; - ApplicationPaths = applicationPaths; - TaskManager = taskManager; - JsonSerializer = jsonSerializer; - Logger = logger; + _applicationPaths = applicationPaths; + _taskManager = taskManager; + _jsonSerializer = jsonSerializer; + _logger = logger; InitTriggerEvents(); } - private bool _readFromFile = false; - /// <summary> - /// The _last execution result. - /// </summary> - private TaskResult _lastExecutionResult; + public event EventHandler<GenericEventArgs<double>> TaskProgress; + /// <summary> - /// The _last execution result sync lock. + /// Gets the scheduled task. /// </summary> - private readonly object _lastExecutionResultSyncLock = new object(); + /// <value>The scheduled task.</value> + public IScheduledTask ScheduledTask { get; private set; } + /// <summary> /// Gets the last execution result. /// </summary> @@ -136,11 +150,11 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - _lastExecutionResult = JsonSerializer.DeserializeFromFile<TaskResult>(path); + _lastExecutionResult = _jsonSerializer.DeserializeFromFile<TaskResult>(path); } catch (Exception ex) { - Logger.LogError(ex, "Error deserializing {File}", path); + _logger.LogError(ex, "Error deserializing {File}", path); } } @@ -160,7 +174,7 @@ namespace Emby.Server.Implementations.ScheduledTasks lock (_lastExecutionResultSyncLock) { - JsonSerializer.SerializeToFile(value, path); + _jsonSerializer.SerializeToFile(value, path); } } } @@ -184,7 +198,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public string Category => ScheduledTask.Category; /// <summary> - /// Gets the current cancellation token. + /// Gets or sets the current cancellation token. /// </summary> /// <value>The current cancellation token source.</value> private CancellationTokenSource CurrentCancellationTokenSource { get; set; } @@ -221,12 +235,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public double? CurrentProgress { get; private set; } /// <summary> - /// The _triggers. - /// </summary> - private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers; - - /// <summary> - /// Gets the triggers that define when the task will run. + /// Gets or sets the triggers that define when the task will run. /// </summary> /// <value>The triggers.</value> private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers @@ -255,7 +264,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Gets the triggers that define when the task will run. /// </summary> /// <value>The triggers.</value> - /// <exception cref="ArgumentNullException">value</exception> + /// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception> public TaskTriggerInfo[] Triggers { get @@ -281,11 +290,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// The _id. - /// </summary> - private string _id; - - /// <summary> /// Gets the unique id. /// </summary> /// <value>The unique id.</value> @@ -325,9 +329,9 @@ namespace Emby.Server.Implementations.ScheduledTasks trigger.Stop(); - trigger.Triggered -= trigger_Triggered; - trigger.Triggered += trigger_Triggered; - trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup); + trigger.Triggered -= OnTriggerTriggered; + trigger.Triggered += OnTriggerTriggered; + trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup); } } @@ -336,7 +340,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - async void trigger_Triggered(object sender, EventArgs e) + private async void OnTriggerTriggered(object sender, EventArgs e) { var trigger = (ITaskTrigger)sender; @@ -347,19 +351,17 @@ namespace Emby.Server.Implementations.ScheduledTasks return; } - Logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); + _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name); trigger.Stop(); - TaskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); + _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions); await Task.Delay(1000).ConfigureAwait(false); - trigger.Start(LastExecutionResult, Logger, Name, false); + trigger.Start(LastExecutionResult, _logger, Name, false); } - private Task _currentTask; - /// <summary> /// Executes the task. /// </summary> @@ -395,9 +397,9 @@ namespace Emby.Server.Implementations.ScheduledTasks CurrentCancellationTokenSource = new CancellationTokenSource(); - Logger.LogInformation("Executing {0}", Name); + _logger.LogInformation("Executing {0}", Name); - ((TaskManager)TaskManager).OnTaskExecuting(this); + ((TaskManager)_taskManager).OnTaskExecuting(this); progress.ProgressChanged += OnProgressChanged; @@ -423,7 +425,7 @@ namespace Emby.Server.Implementations.ScheduledTasks } catch (Exception ex) { - Logger.LogError(ex, "Error"); + _logger.LogError(ex, "Error"); failureException = ex; @@ -476,7 +478,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { if (State == TaskState.Running) { - Logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); + _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name); CurrentCancellationTokenSource.Cancel(); } } @@ -487,7 +489,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <returns>System.String.</returns> private string GetScheduledTasksConfigurationDirectory() { - return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); } /// <summary> @@ -496,7 +498,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <returns>System.String.</returns> private string GetScheduledTasksDataDirectory() { - return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks"); + return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks"); } /// <summary> @@ -535,7 +537,7 @@ namespace Emby.Server.Implementations.ScheduledTasks TaskTriggerInfo[] list = null; if (File.Exists(path)) { - list = JsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path); + list = _jsonSerializer.DeserializeFromFile<TaskTriggerInfo[]>(path); } // Return defaults if file doesn't exist. @@ -571,7 +573,7 @@ namespace Emby.Server.Implementations.ScheduledTasks Directory.CreateDirectory(Path.GetDirectoryName(path)); - JsonSerializer.SerializeToFile(triggers, path); + _jsonSerializer.SerializeToFile(triggers, path); } /// <summary> @@ -585,7 +587,7 @@ namespace Emby.Server.Implementations.ScheduledTasks { var elapsedTime = endTime - startTime; - Logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); + _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); var result = new TaskResult { @@ -606,7 +608,7 @@ namespace Emby.Server.Implementations.ScheduledTasks LastExecutionResult = result; - ((TaskManager)TaskManager).OnTaskCompleted(this, result); + ((TaskManager)_taskManager).OnTaskCompleted(this, result); } /// <summary> @@ -615,6 +617,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// <summary> @@ -635,12 +638,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogInformation(Name + ": Cancelling"); + _logger.LogInformation(Name + ": Cancelling"); token.Cancel(); } catch (Exception ex) { - Logger.LogError(ex, "Error calling CancellationToken.Cancel();"); + _logger.LogError(ex, "Error calling CancellationToken.Cancel();"); } } @@ -649,21 +652,21 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogInformation(Name + ": Waiting on Task"); + _logger.LogInformation(Name + ": Waiting on Task"); var exited = Task.WaitAll(new[] { task }, 2000); if (exited) { - Logger.LogInformation(Name + ": Task exited"); + _logger.LogInformation(Name + ": Task exited"); } else { - Logger.LogInformation(Name + ": Timed out waiting for task to stop"); + _logger.LogInformation(Name + ": Timed out waiting for task to stop"); } } catch (Exception ex) { - Logger.LogError(ex, "Error calling Task.WaitAll();"); + _logger.LogError(ex, "Error calling Task.WaitAll();"); } } @@ -671,12 +674,12 @@ namespace Emby.Server.Implementations.ScheduledTasks { try { - Logger.LogDebug(Name + ": Disposing CancellationToken"); + _logger.LogDebug(Name + ": Disposing CancellationToken"); token.Dispose(); } catch (Exception ex) { - Logger.LogError(ex, "Error calling CancellationToken.Dispose();"); + _logger.LogError(ex, "Error calling CancellationToken.Dispose();"); } } @@ -692,8 +695,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="info">The info.</param> /// <returns>BaseTaskTrigger.</returns> - /// <exception cref="ArgumentNullException"></exception> - /// <exception cref="ArgumentException">Invalid trigger type: + info.Type</exception> + /// <exception cref="ArgumentException">Invalid trigger type: + info.Type.</exception> private ITaskTrigger GetTrigger(TaskTriggerInfo info) { var options = new TaskOptions @@ -701,7 +703,7 @@ namespace Emby.Server.Implementations.ScheduledTasks MaxRuntimeTicks = info.MaxRuntimeTicks }; - if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase)) { if (!info.TimeOfDayTicks.HasValue) { @@ -715,7 +717,7 @@ namespace Emby.Server.Implementations.ScheduledTasks }; } - if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase)) { if (!info.TimeOfDayTicks.HasValue) { @@ -735,7 +737,7 @@ namespace Emby.Server.Implementations.ScheduledTasks }; } - if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase)) + if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase)) { if (!info.IntervalTicks.HasValue) { @@ -749,7 +751,7 @@ namespace Emby.Server.Implementations.ScheduledTasks }; } - if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase)) + if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase)) { return new StartupTrigger(); } @@ -765,7 +767,7 @@ namespace Emby.Server.Implementations.ScheduledTasks foreach (var triggerInfo in InternalTriggers) { var trigger = triggerInfo.Item2; - trigger.Triggered -= trigger_Triggered; + trigger.Triggered -= OnTriggerTriggered; trigger.Stop(); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 81096026b..6f81bf49b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -5,8 +5,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -207,6 +207,7 @@ namespace Emby.Server.Implementations.ScheduledTasks public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } /// <summary> diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs new file mode 100644 index 000000000..4abbf784b --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks +{ + /// <summary> + /// Deletes old activity log entries. + /// </summary> + public class CleanActivityLogTask : IScheduledTask, IConfigurableScheduledTask + { + private readonly ILocalizationManager _localization; + private readonly IActivityManager _activityManager; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="CleanActivityLogTask"/> class. + /// </summary> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> + /// <param name="activityManager">Instance of the <see cref="IActivityManager"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public CleanActivityLogTask( + ILocalizationManager localization, + IActivityManager activityManager, + IServerConfigurationManager serverConfigurationManager) + { + _localization = localization; + _activityManager = activityManager; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanActivityLog"); + + /// <inheritdoc /> + public string Key => "CleanActivityLog"; + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanActivityLogDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + + /// <inheritdoc /> + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + { + var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; + if (!retentionDays.HasValue || retentionDays <= 0) + { + throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); + } + + var startDate = DateTime.UtcNow.AddDays(retentionDays.Value * -1); + return _activityManager.CleanAsync(startDate); + } + + /// <inheritdoc /> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() + { + return Enumerable.Empty<TaskTriggerInfo>(); + } + } +}
\ No newline at end of file diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index e29fcfb5f..692d1667d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - private IApplicationPaths ApplicationPaths { get; set; } - + private readonly IApplicationPaths _applicationPaths; private readonly ILogger<DeleteCacheFileTask> _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -37,22 +35,43 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IFileSystem fileSystem, ILocalizationManager localization) { - ApplicationPaths = appPaths; + _applicationPaths = appPaths; _logger = logger; _fileSystem = fileSystem; _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCache"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => "DeleteCacheFiles"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } @@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - /// <summary> /// Deletes the cache files from directory with a last write time less than a given date. /// </summary> @@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _logger.LogError(ex, "Error deleting file {path}", path); } } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanCache"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public string Key => "DeleteCacheFiles"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 402b39a26..184d155d4 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -1,12 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -15,12 +16,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// </summary> public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask { - /// <summary> - /// Gets or sets the configuration manager. - /// </summary> - /// <value>The configuration manager.</value> - private IConfigurationManager ConfigurationManager { get; set; } - + private readonly IConfigurationManager _configurationManager; private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -32,19 +28,44 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <param name="localization">The localization manager.</param> public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) { - ConfigurationManager = configurationManager; + _configurationManager = configurationManager; _fileSystem = fileSystem; _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanLogs"); + + /// <inheritdoc /> + public string Description => string.Format( + CultureInfo.InvariantCulture, + _localization.GetLocalizedString("TaskCleanLogsDescription"), + _configurationManager.CommonConfiguration.LogFileRetentionDays); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => "CleanLogFiles"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} + return new[] + { + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } @@ -57,10 +78,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { // Delete log files more than n days old - var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays); + var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); // Only delete the .txt log files, the *.log files created by serilog get managed by itself - var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true) + var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true) .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) .ToList(); @@ -83,26 +104,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanLogs"); - - /// <inheritdoc /> - public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public string Key => "CleanLogFiles"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 691408167..26ef19354 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -148,7 +148,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public bool IsHidden => false; /// <inheritdoc /> - public bool IsEnabled => false; + public bool IsEnabled => true; /// <inheritdoc /> public bool IsLogged => true; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 7388086fb..c5af68bce 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); + + /// <inheritdoc /> + public string Key => "PluginUpdates"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> @@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks progress.Report(100); } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); - - /// <inheritdoc /> - public string Key => "PluginUpdates"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index eb628ec5f..8b67d37d7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class DailyTrigger : ITaskTrigger { /// <summary> - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + + /// <summary> + /// Gets or sets the time of day to trigger the task to run. /// </summary> /// <value>The time of day.</value> public TimeSpan TimeOfDay { get; set; } @@ -70,11 +75,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 247a6785a..b04fd7c7e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class IntervalTrigger : ITaskTrigger { + private DateTime _lastStartDate; + + /// <summary> + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + /// <summary> /// Gets or sets the interval. /// </summary> @@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <value>The timer.</value> private Timer Timer { get; set; } - private DateTime _lastStartDate; - /// <summary> /// Stars waiting for the trigger action. /// </summary> @@ -89,11 +94,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 96e5d8897..7cd5493da 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class StartupTrigger : ITaskTrigger { + /// <summary> + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + public int DelayMs { get; set; } /// <summary> @@ -49,19 +54,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 4f1bf5c19..0c0ebec08 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class WeeklyTrigger : ITaskTrigger { /// <summary> - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + + /// <summary> + /// Gets or sets the time of day to trigger the task to run. /// </summary> /// <value>The time of day.</value> public TimeSpan TimeOfDay { get; set; } @@ -96,19 +101,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 4dfadc703..4bc12f44a 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -54,7 +54,8 @@ namespace Emby.Server.Implementations.Security { if (tableNewlyCreated && TableExists(connection, "AccessTokens")) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { var existingColumnNames = GetColumnNames(db, "AccessTokens"); @@ -88,7 +89,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("insert into Tokens (AccessToken, DeviceId, AppName, AppVersion, DeviceName, UserId, UserName, IsActive, DateCreated, DateLastActivity) values (@AccessToken, @DeviceId, @AppName, @AppVersion, @DeviceName, @UserId, @UserName, @IsActive, @DateCreated, @DateLastActivity)")) { @@ -119,7 +121,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("Update Tokens set AccessToken=@AccessToken, DeviceId=@DeviceId, AppName=@AppName, AppVersion=@AppVersion, DeviceName=@DeviceName, UserId=@UserId, UserName=@UserName, DateCreated=@DateCreated, DateLastActivity=@DateLastActivity where Id=@Id")) { @@ -151,7 +154,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("Delete from Tokens where Id=@Id")) { @@ -257,8 +261,7 @@ namespace Emby.Server.Implementations.Security connection.RunInTransaction( db => { - var statements = PrepareAll(db, statementTexts) - .ToList(); + var statements = PrepareAll(db, statementTexts); using (var statement = statements[0]) { @@ -282,7 +285,7 @@ namespace Emby.Server.Implementations.Security ReadTransactionMode); } - result.Items = list.ToArray(); + result.Items = list; return result; } @@ -347,7 +350,8 @@ namespace Emby.Server.Implementations.Security { using (var connection = GetConnection(true)) { - return connection.RunInTransaction(db => + return connection.RunInTransaction( + db => { using (var statement = base.PrepareStatement(db, "select CustomName from Devices where Id=@DeviceId")) { @@ -378,7 +382,8 @@ namespace Emby.Server.Implementations.Security using (var connection = GetConnection()) { - connection.RunInTransaction(db => + connection.RunInTransaction( + db => { using (var statement = db.PrepareStatement("replace into devices (Id, CustomName, Capabilities) VALUES (@Id, @CustomName, (Select Capabilities from Devices where Id=@Id))")) { diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs index dfdd4200e..ac589b03c 100644 --- a/Emby.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Implementations/ServerApplicationPaths.cs @@ -104,6 +104,6 @@ namespace Emby.Server.Implementations public string InternalMetadataPath { get; set; } /// <inheritdoc /> - public string VirtualInternalMetadataPath { get; } = "%MetadataPath%"; + public string VirtualInternalMetadataPath => "%MetadataPath%"; } } diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs deleted file mode 100644 index 8ba86f756..000000000 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public class HttpResult - : IHttpResult, IAsyncStreamWriter - { - public HttpResult(object response, string contentType, HttpStatusCode statusCode) - { - this.Headers = new Dictionary<string, string>(); - - this.Response = response; - this.ContentType = contentType; - this.StatusCode = statusCode; - } - - public object Response { get; set; } - - public string ContentType { get; set; } - - public IDictionary<string, string> Headers { get; private set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - public IRequest RequestContext { get; set; } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - var response = RequestContext?.Response; - - if (this.Response is byte[] bytesResponse) - { - var contentLength = bytesResponse.Length; - - if (response != null) - { - response.ContentLength = contentLength; - } - - if (contentLength > 0) - { - await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); - } - - return; - } - - await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false); - } - } -} diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs deleted file mode 100644 index 1f9c7fc22..000000000 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; - -namespace Emby.Server.Implementations.Services -{ - public class RequestHelper - { - public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.DeserializeXml; - - case "application/json": - case "text/json": - return host.DeserializeJson; - } - - return null; - } - - public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.SerializeToXml; - - case "application/json": - case "text/json": - return host.SerializeToJson; - } - - return null; - } - - private static string GetContentTypeWithoutEncoding(string contentType) - { - return contentType?.Split(';')[0].ToLowerInvariant().Trim(); - } - } -} diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs deleted file mode 100644 index a329b531d..000000000 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Services -{ - public static class ResponseHelper - { - public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken) - { - if (result == null) - { - if (response.StatusCode == (int)HttpStatusCode.OK) - { - response.StatusCode = (int)HttpStatusCode.NoContent; - } - - response.ContentLength = 0; - return Task.CompletedTask; - } - - var httpResult = result as IHttpResult; - if (httpResult != null) - { - httpResult.RequestContext = request; - request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType; - } - - var defaultContentType = request.ResponseContentType; - - if (httpResult != null) - { - if (httpResult.RequestContext == null) - { - httpResult.RequestContext = request; - } - - response.StatusCode = httpResult.Status; - } - - if (result is IHasHeaders responseOptions) - { - foreach (var responseHeaders in responseOptions.Headers) - { - if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) - { - response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); - continue; - } - - response.Headers.Add(responseHeaders.Key, responseHeaders.Value); - } - } - - // ContentType='text/html' is the default for a HttpResponse - // Do not override if another has been set - if (response.ContentType == null || response.ContentType == "text/html") - { - response.ContentType = defaultContentType; - } - - if (response.ContentType == "application/json") - { - response.ContentType += "; charset=utf-8"; - } - - switch (result) - { - case IAsyncStreamWriter asyncStreamWriter: - return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken); - case IStreamWriter streamWriter: - streamWriter.WriteTo(response.Body); - return Task.CompletedTask; - case FileWriter fileWriter: - return fileWriter.WriteToAsync(response, cancellationToken); - case Stream stream: - return CopyStream(stream, response.Body); - case byte[] bytes: - response.ContentType = "application/octet-stream"; - response.ContentLength = bytes.Length; - - if (bytes.Length > 0) - { - return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - - return Task.CompletedTask; - case string responseText: - var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); - response.ContentLength = responseTextAsBytes.Length; - - if (responseTextAsBytes.Length > 0) - { - return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); - } - - return Task.CompletedTask; - } - - return WriteObject(request, result, response); - } - - private static async Task CopyStream(Stream src, Stream dest) - { - using (src) - { - await src.CopyToAsync(dest).ConfigureAwait(false); - } - } - - public static async Task WriteObject(IRequest request, object result, HttpResponse response) - { - var contentType = request.ResponseContentType; - var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - using (var ms = new MemoryStream()) - { - serializer(result, ms); - - ms.Position = 0; - - var contentLength = ms.Length; - response.ContentLength = contentLength; - - if (contentLength > 0) - { - await ms.CopyToAsync(response.Body).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs deleted file mode 100644 index 47e7261e8..000000000 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ /dev/null @@ -1,202 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public delegate object ActionInvokerFn(object intance, object request); - - public delegate void VoidActionInvokerFn(object intance, object request); - - public class ServiceController - { - private readonly ILogger<ServiceController> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="ServiceController"/> class. - /// </summary> - /// <param name="logger">The <see cref="ServiceController"/> logger.</param> - public ServiceController(ILogger<ServiceController> logger) - { - _logger = logger; - } - - public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes) - { - foreach (var serviceType in serviceTypes) - { - RegisterService(appHost, serviceType); - } - } - - public void RegisterService(HttpListenerHost appHost, Type serviceType) - { - // Make sure the provided type implements IService - if (!typeof(IService).IsAssignableFrom(serviceType)) - { - _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType); - return; - } - - var processedReqs = new HashSet<Type>(); - - var actions = ServiceExecGeneral.Reset(serviceType); - - foreach (var mi in serviceType.GetActions()) - { - var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) - { - continue; - } - - processedReqs.Add(requestType); - - ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); - - // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>)); - // var responseType = returnMarker != null ? - // GetGenericArguments(returnMarker)[0] - // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? - // mi.ReturnType - // : Type.GetType(requestType.FullName + "Response"); - - RegisterRestPaths(appHost, requestType, serviceType); - - appHost.AddServiceInfo(serviceType, requestType); - } - } - - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); - - public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) - { - var attrs = appHost.GetRouteAttributes(requestType); - foreach (var attr in attrs) - { - var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); - - RegisterRestPath(restPath); - } - } - - private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; - - public void RegisterRestPath(RestPath restPath) - { - if (restPath.Path[0] != '/') - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' must start with a '/'", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' contains invalid chars. ", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch)) - { - pathsAtFirstMatch.Add(restPath); - } - else - { - RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath }; - } - } - - public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) - { - var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); - - List<RestPath> firstMatches; - - var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedHashMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedWildcardMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - return null; - } - - public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req) - { - var requestType = requestDto.GetType(); - req.OperationName = requestType.Name; - - var serviceType = httpHost.GetServiceTypeByRequest(requestType); - - var service = httpHost.CreateInstance(serviceType); - - if (service is IRequiresRequest serviceRequiresContext) - { - serviceRequiresContext.Request = req; - } - - // Executes the service and returns the result - return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs deleted file mode 100644 index 7b970627e..000000000 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ /dev/null @@ -1,230 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public static class ServiceExecExtensions - { - public static string[] AllVerbs = new[] { - "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 - "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 - "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", - "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 - "ORDERPATCH", // RFC 3648 - "ACL", // RFC 3744 - "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ - "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ - "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", - "POLL", "SUBSCRIBE", "UNSUBSCRIBE" - }; - - public static List<MethodInfo> GetActions(this Type serviceType) - { - var list = new List<MethodInfo>(); - - foreach (var mi in serviceType.GetRuntimeMethods()) - { - if (!mi.IsPublic) - { - continue; - } - - if (mi.IsStatic) - { - continue; - } - - if (mi.GetParameters().Length != 1) - { - continue; - } - - var actionName = mi.Name; - if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - list.Add(mi); - } - - return list; - } - } - - internal static class ServiceExecGeneral - { - private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>(); - - public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions) - { - foreach (var actionCtx in actions) - { - if (execMap.ContainsKey(actionCtx.Id)) - { - continue; - } - - execMap[actionCtx.Id] = actionCtx; - } - } - - public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) - { - var actionName = request.Verb ?? "POST"; - - if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext)) - { - if (actionContext.RequestFilters != null) - { - foreach (var requestFilter in actionContext.RequestFilters) - { - requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.HasStarted) - { - Task.FromResult<object>(null); - } - } - } - - var response = actionContext.ServiceAction(instance, requestDto); - - if (response is Task taskResponse) - { - return GetTaskResult(taskResponse); - } - - return Task.FromResult(response); - } - - var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException( - string.Format( - CultureInfo.InvariantCulture, - "Could not find method named {1}({0}) or Any({0}) on Service {2}", - requestDto.GetType().GetMethodName(), - expectedMethodName, - serviceType.GetMethodName())); - } - - private static async Task<object> GetTaskResult(Task task) - { - try - { - if (task is Task<object> taskObject) - { - return await taskObject.ConfigureAwait(false); - } - - await task.ConfigureAwait(false); - - var type = task.GetType().GetTypeInfo(); - if (!type.IsGenericType) - { - return null; - } - - var resultProperty = type.GetDeclaredProperty("Result"); - if (resultProperty == null) - { - return null; - } - - var result = resultProperty.GetValue(task); - - // hack alert - if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1) - { - return null; - } - - return result; - } - catch (TypeAccessException) - { - return null; // return null for void Task's - } - } - - public static List<ServiceMethod> Reset(Type serviceType) - { - var actions = new List<ServiceMethod>(); - - foreach (var mi in serviceType.GetActions()) - { - var actionName = mi.Name; - var args = mi.GetParameters(); - - var requestType = args[0].ParameterType; - var actionCtx = new ServiceMethod - { - Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName()) - }; - - actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); - - var reqFilters = new List<IHasRequestFilter>(); - - foreach (var attr in mi.GetCustomAttributes(true)) - { - if (attr is IHasRequestFilter hasReqFilter) - { - reqFilters.Add(hasReqFilter); - } - } - - if (reqFilters.Count > 0) - { - actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); - } - - actions.Add(actionCtx); - } - - return actions; - } - - private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) - { - var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); - var serviceStrong = Expression.Convert(serviceParam, serviceType); - - var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); - var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); - - Expression callExecute = Expression.Call( - serviceStrong, mi, requestDtoStrong); - - if (mi.ReturnType != typeof(void)) - { - var executeFunc = Expression.Lambda<ActionInvokerFn>( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return executeFunc; - } - else - { - var executeFunc = Expression.Lambda<VoidActionInvokerFn>( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return (service, request) => - { - executeFunc(service, request); - return null; - }; - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs deleted file mode 100644 index b4166f771..000000000 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Mime; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceHandler - { - private RestPath _restPath; - - private string _responseContentType; - - internal ServiceHandler(RestPath restPath, string responseContentType) - { - _restPath = restPath; - _responseContentType = responseContentType; - } - - protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) - { - if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) - { - var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer.Invoke(requestType, httpReq.InputStream); - } - } - - return Task.FromResult(host.CreateInstance(requestType)); - } - - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) - { - contentType = null; - var pos = pathInfo.LastIndexOf('.'); - if (pos != -1) - { - var format = pathInfo.AsSpan().Slice(pos + 1); - contentType = GetFormatContentType(format); - if (contentType != null) - { - pathInfo = pathInfo.Substring(0, pos); - } - } - - return pathInfo; - } - - private static string GetFormatContentType(ReadOnlySpan<char> format) - { - if (format.Equals("json", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Json; - } - else if (format.Equals("xml", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Xml; - } - - return null; - } - - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken) - { - httpReq.Items["__route"] = _restPath; - - if (_responseContentType != null) - { - httpReq.ResponseContentType = _responseContentType; - } - - var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false); - - httpHost.ApplyRequestFilters(httpReq, httpRes, request); - - httpRes.HttpContext.SetServiceStackRequest(httpReq); - var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); - - // Apply response filters - foreach (var responseFilter in httpHost.ResponseFilters) - { - responseFilter(httpReq, httpRes, response); - } - - await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); - } - - public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath) - { - var requestType = restPath.RequestType; - - if (RequireqRequestStream(requestType)) - { - // Used by IRequiresRequestStream - var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request); - var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); - - var rawReq = (IRequiresRequestStream)request; - rawReq.RequestStream = httpReq.InputStream; - return rawReq; - } - else - { - var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request); - - var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); - - return CreateRequest(httpReq, restPath, requestParams, requestDto); - } - } - - public static bool RequireqRequestStream(Type requestType) - { - var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); - - return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()); - } - - public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto) - { - var pathInfo = !restPath.IsWildCardPath - ? GetSanitizedPathInfo(httpReq.PathInfo, out _) - : httpReq.PathInfo; - - return restPath.CreateRequest(pathInfo, requestParams, requestDto); - } - - /// <summary> - /// Duplicate Params are given a unique key by appending a #1 suffix - /// </summary> - private static Dictionary<string, string> GetRequestParams(HttpRequest request) - { - var map = new Dictionary<string, string>(); - - foreach (var pair in request.Query) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - } - - return map; - } - - private static bool IsMethod(string method, string expected) - => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); - - /// <summary> - /// Duplicate params have their values joined together in a comma-delimited string. - /// </summary> - private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request) - { - var map = new Dictionary<string, string>(); - - foreach (var pair in request.Query) - { - map[pair.Key] = pair.Value; - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - map[pair.Key] = pair.Value; - } - } - - return map; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs deleted file mode 100644 index 5116cc04f..000000000 --- a/Emby.Server.Implementations/Services/ServiceMethod.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceMethod - { - public string Id { get; set; } - - public ActionInvokerFn ServiceAction { get; set; } - - public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } - - public static string Key(Type serviceType, string method, string requestDtoName) - { - return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs deleted file mode 100644 index 442b2ab1c..000000000 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ /dev/null @@ -1,550 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.Services -{ - public class RestPath - { - private const string WildCard = "*"; - private const char WildCardChar = '*'; - private const string PathSeperator = "/"; - private const char PathSeperatorChar = '/'; - private const char ComponentSeperator = '.'; - private const string VariablePrefix = "{"; - - private readonly bool[] componentsWithSeparators; - - private readonly string restPath; - public bool IsWildCardPath { get; private set; } - - private readonly string[] literalsToMatch; - - private readonly string[] variablesNames; - - private readonly bool[] isWildcard; - private readonly int wildcardCount = 0; - - internal static string[] IgnoreAttributesNamed = new[] - { - nameof(JsonIgnoreAttribute) - }; - - private static Type _excludeType = typeof(Stream); - - public int VariableArgsCount { get; set; } - - /// <summary> - /// The number of segments separated by '/' determinable by path.Split('/').Length - /// e.g. /path/to/here.ext == 3 - /// </summary> - public int PathComponentsCount { get; set; } - - /// <summary> - /// Gets or sets the total number of segments after subparts have been exploded ('.') - /// e.g. /path/to/here.ext == 4. - /// </summary> - public int TotalComponentsCount { get; set; } - - public string[] Verbs { get; private set; } - - public Type RequestType { get; private set; } - - public Type ServiceType { get; private set; } - - public string Path => this.restPath; - - public string Summary { get; private set; } - - public string Description { get; private set; } - - public bool IsHidden { get; private set; } - - public static string[] GetPathPartsForMatching(string pathInfo) - { - return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - } - - public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching) - { - var hashPrefix = pathPartsForMatching.Length + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) - { - const string hashPrefix = WildCard + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) - { - var list = new List<string>(); - - foreach (var part in pathPartsForMatching) - { - list.Add(hashPrefix + part); - - if (part.IndexOf(ComponentSeperator) == -1) - { - continue; - } - - var subParts = part.Split(ComponentSeperator); - foreach (var subPart in subParts) - { - list.Add(hashPrefix + subPart); - } - } - - return list; - } - - public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) - { - this.RequestType = requestType; - this.ServiceType = serviceType; - this.Summary = summary; - this.IsHidden = isHidden; - this.Description = description; - this.restPath = path; - - this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); - - var componentsList = new List<string>(); - - // We only split on '.' if the restPath has them. Allows for /{action}.{type} - var hasSeparators = new List<bool>(); - foreach (var component in this.restPath.Split(PathSeperatorChar)) - { - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 - && component.IndexOf(ComponentSeperator) != -1) - { - hasSeparators.Add(true); - componentsList.AddRange(component.Split(ComponentSeperator)); - } - else - { - hasSeparators.Add(false); - componentsList.Add(component); - } - } - - var components = componentsList.ToArray(); - this.TotalComponentsCount = components.Length; - - this.literalsToMatch = new string[this.TotalComponentsCount]; - this.variablesNames = new string[this.TotalComponentsCount]; - this.isWildcard = new bool[this.TotalComponentsCount]; - this.componentsWithSeparators = hasSeparators.ToArray(); - this.PathComponentsCount = this.componentsWithSeparators.Length; - string firstLiteralMatch = null; - - for (var i = 0; i < components.Length; i++) - { - var component = components[i]; - - if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) - { - var variableName = component.Substring(1, component.Length - 2); - if (variableName[variableName.Length - 1] == WildCardChar) - { - this.isWildcard[i] = true; - variableName = variableName.Substring(0, variableName.Length - 1); - } - - this.variablesNames[i] = variableName; - this.VariableArgsCount++; - } - else - { - this.literalsToMatch[i] = component.ToLowerInvariant(); - - if (firstLiteralMatch == null) - { - firstLiteralMatch = this.literalsToMatch[i]; - } - } - } - - for (var i = 0; i < components.Length - 1; i++) - { - if (!this.isWildcard[i]) - { - continue; - } - - if (this.literalsToMatch[i + 1] == null) - { - throw new ArgumentException( - "A wildcard path component must be at the end of the path or followed by a literal path component."); - } - } - - this.wildcardCount = this.isWildcard.Length; - this.IsWildCardPath = this.wildcardCount > 0; - - this.FirstMatchHashKey = !this.IsWildCardPath - ? this.PathComponentsCount + PathSeperator + firstLiteralMatch - : WildCardChar + PathSeperator + firstLiteralMatch; - - this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - - _propertyNamesMap = new HashSet<string>( - GetSerializableProperties(RequestType).Select(x => x.Name), - StringComparer.OrdinalIgnoreCase); - } - - internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type) - { - foreach (var prop in GetPublicProperties(type)) - { - if (prop.GetMethod == null - || _excludeType == prop.PropertyType) - { - continue; - } - - var ignored = false; - foreach (var attr in prop.GetCustomAttributes(true)) - { - if (IgnoreAttributesNamed.Contains(attr.GetType().Name)) - { - ignored = true; - break; - } - } - - if (!ignored) - { - yield return prop; - } - } - } - - private static IEnumerable<PropertyInfo> GetPublicProperties(Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List<PropertyInfo>(); - var considered = new List<Type>() - { - type - }; - var queue = new Queue<Type>(); - queue.Enqueue(type); - - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) - { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - var newPropertyInfos = GetTypesPublicProperties(subType) - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos; - } - - return GetTypesPublicProperties(type) - .Where(x => x.GetIndexParameters().Length == 0); - } - - private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType) - { - foreach (var pi in subType.GetRuntimeProperties()) - { - var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) - { - continue; - } - - yield return pi; - } - } - - /// <summary> - /// Provide for quick lookups based on hashes that can be determined from a request url. - /// </summary> - public string FirstMatchHashKey { get; private set; } - - private readonly StringMapTypeDeserializer typeDeserializer; - - private readonly HashSet<string> _propertyNamesMap; - - public int MatchScore(string httpMethod, string[] withPathInfoParts) - { - var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount); - if (!isMatch) - { - return -1; - } - - // Routes with least wildcard matches get the highest score - var score = Math.Max(100 - wildcardMatchCount, 1) * 1000 - // Routes with less variable (and more literal) matches - + Math.Max(10 - VariableArgsCount, 1) * 100; - - // Exact verb match is better than ANY - if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) - { - score += 10; - } - else - { - score += 1; - } - - return score; - } - - /// <summary> - /// For performance withPathInfoParts should already be a lower case string - /// to minimize redundant matching operations. - /// </summary> - public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) - { - wildcardMatchCount = 0; - - if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) - { - return false; - } - - if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - if (!ExplodeComponents(ref withPathInfoParts)) - { - return false; - } - - if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) - { - return false; - } - - int pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - if (this.isWildcard[i]) - { - if (i < this.TotalComponentsCount - 1) - { - // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length - && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) - { - pathIx++; - wildcardMatchCount++; - } - - // Ensure there are still enough parts left to match the remainder - if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) - { - return false; - } - } - else - { - // A wildcard at the end matches the remainder of path - wildcardMatchCount += withPathInfoParts.Length - pathIx; - pathIx = withPathInfoParts.Length; - } - } - else - { - var literalToMatch = this.literalsToMatch[i]; - if (literalToMatch == null) - { - // Matching an ordinary (non-wildcard) variable consumes a single part - pathIx++; - continue; - } - - if (withPathInfoParts.Length <= pathIx - || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - pathIx++; - } - } - - return pathIx == withPathInfoParts.Length; - } - - private bool ExplodeComponents(ref string[] withPathInfoParts) - { - var totalComponents = new List<string>(); - for (var i = 0; i < withPathInfoParts.Length; i++) - { - var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (this.PathComponentsCount != this.TotalComponentsCount - && this.componentsWithSeparators[i]) - { - var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) - { - return false; - } - - totalComponents.AddRange(subComponents); - } - else - { - totalComponents.Add(component); - } - } - - withPathInfoParts = totalComponents.ToArray(); - return true; - } - - public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance) - { - var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - - ExplodeComponents(ref requestComponents); - - if (requestComponents.Length != this.TotalComponentsCount) - { - var isValidWildCardPath = this.IsWildCardPath - && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; - - if (!isValidWildCardPath) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", - pathInfo, - this.restPath)); - } - } - - var requestKeyValuesMap = new Dictionary<string, string>(); - var pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - var variableName = this.variablesNames[i]; - if (variableName == null) - { - pathIx++; - continue; - } - - if (!this._propertyNamesMap.Contains(variableName)) - { - if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) - { - pathIx++; - continue; - } - - throw new ArgumentException("Could not find property " - + variableName + " on " + RequestType.GetMethodName()); - } - - var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch - if (value != null && this.isWildcard[i]) - { - if (i == this.TotalComponentsCount - 1) - { - // Wildcard at end of path definition consumes all the rest - var sb = new StringBuilder(); - sb.Append(value); - for (var j = pathIx + 1; j < requestComponents.Length; j++) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[j]); - } - - value = sb.ToString(); - } - else - { - // Wildcard in middle of path definition consumes up until it - // hits a match for the next element in the definition (which must be a literal) - // It may consume 0 or more path parts - var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; - if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - var sb = new StringBuilder(value); - pathIx++; - while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[pathIx++]); - } - - value = sb.ToString(); - } - else - { - value = null; - } - } - } - else - { - // Variable consumes single path item - pathIx++; - } - - requestKeyValuesMap[variableName] = value; - } - - if (queryStringAndFormData != null) - { - // Query String and form data can override variable path matches - // path variables < query string < form data - foreach (var name in queryStringAndFormData) - { - requestKeyValuesMap[name.Key] = name.Value; - } - } - - return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); - } - - public class RestPathMap : SortedDictionary<string, List<RestPath>> - { - public RestPathMap() : base(StringComparer.OrdinalIgnoreCase) - { - } - } - } -} diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs deleted file mode 100644 index 165bb0fc4..000000000 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Reflection; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// <summary> - /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) - /// </summary> - public class StringMapTypeDeserializer - { - internal class PropertySerializerEntry - { - public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType) - { - PropertySetFn = propertySetFn; - PropertyParseStringFn = propertyParseStringFn; - PropertyType = propertyType; - } - - public Action<object, object> PropertySetFn { get; private set; } - - public Func<string, object> PropertyParseStringFn { get; private set; } - - public Type PropertyType { get; private set; } - } - - private readonly Type type; - private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap - = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase); - - public Func<string, object> GetParseFn(Type propertyType) - { - if (propertyType == typeof(string)) - { - return s => s; - } - - return _GetParseFn(propertyType); - } - - private readonly Func<Type, object> _CreateInstanceFn; - private readonly Func<Type, Func<string, object>> _GetParseFn; - - public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type) - { - _CreateInstanceFn = createInstanceFn; - _GetParseFn = getParseFn; - this.type = type; - - foreach (var propertyInfo in RestPath.GetSerializableProperties(type)) - { - var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo); - var propertyType = propertyInfo.PropertyType; - var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); - - propertySetterMap[propertyInfo.Name] = propertySerializer; - } - } - - public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs) - { - PropertySerializerEntry propertySerializerEntry = null; - - if (instance == null) - { - instance = _CreateInstanceFn(type); - } - - foreach (var pair in keyValuePairs) - { - string propertyName = pair.Key; - string propertyTextValue = pair.Value; - - if (propertyTextValue == null - || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) - || propertySerializerEntry.PropertySetFn == null) - { - continue; - } - - if (propertySerializerEntry.PropertyType == typeof(bool)) - { - // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value - propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString(); - } - - var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); - if (value == null) - { - continue; - } - - propertySerializerEntry.PropertySetFn(instance, value); - } - - return instance; - } - } - - internal static class TypeAccessor - { - public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo) - { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) - { - return null; - } - - var setMethodInfo = propertyInfo.SetMethod; - return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); - } - } -} diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs deleted file mode 100644 index 92e36b60e..000000000 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// <summary> - /// Donated by Ivan Korneliuk from his post: - /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html - /// - /// Modified to only allow using routes matching the supplied HTTP Verb. - /// </summary> - public static class UrlExtensions - { - public static string GetMethodName(this Type type) - { - var typeName = type.FullName != null // can be null, e.g. generic types - ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname - .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces - .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type - : type.Name; - - return type.IsGenericParameter ? "'" + typeName : typeName; - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 862a7296c..607b322f2 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Data.Events; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; @@ -17,6 +18,8 @@ using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Session; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -24,7 +27,6 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Session; @@ -40,25 +42,16 @@ namespace Emby.Server.Implementations.Session /// </summary> public class SessionManager : ISessionManager, IDisposable { - /// <summary> - /// The user data repository. - /// </summary> private readonly IUserDataManager _userDataManager; - - /// <summary> - /// The logger. - /// </summary> private readonly ILogger<SessionManager> _logger; - + private readonly IEventManager _eventManager; private readonly ILibraryManager _libraryManager; private readonly IUserManager _userManager; private readonly IMusicManager _musicManager; private readonly IDtoService _dtoService; private readonly IImageProcessor _imageProcessor; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerApplicationHost _appHost; - private readonly IAuthenticationRepository _authRepo; private readonly IDeviceManager _deviceManager; @@ -75,6 +68,7 @@ namespace Emby.Server.Implementations.Session public SessionManager( ILogger<SessionManager> logger, + IEventManager eventManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IUserManager userManager, @@ -87,6 +81,7 @@ namespace Emby.Server.Implementations.Session IMediaSourceManager mediaSourceManager) { _logger = logger; + _eventManager = eventManager; _userDataManager = userDataManager; _libraryManager = libraryManager; _userManager = userManager; @@ -209,6 +204,8 @@ namespace Emby.Server.Implementations.Session } } + _eventManager.Publish(new SessionStartedEventArgs(info)); + EventHelper.QueueEventIfNotNull( SessionStarted, this, @@ -230,6 +227,8 @@ namespace Emby.Server.Implementations.Session }, _logger); + _eventManager.Publish(new SessionEndedEventArgs(info)); + info.Dispose(); } @@ -667,22 +666,26 @@ namespace Emby.Server.Implementations.Session } } + var eventArgs = new PlaybackStartEventArgs + { + Item = libraryItem, + Users = users, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + // Nothing to save here // Fire events to inform plugins EventHelper.QueueEventIfNotNull( PlaybackStart, this, - new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session - }, + eventArgs, _logger); StartIdleCheckTimer(); @@ -750,23 +753,25 @@ namespace Emby.Server.Implementations.Session } } - PlaybackProgress?.Invoke( - this, - new PlaybackProgressEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = session.PlayState.PositionTicks, - MediaSourceId = session.PlayState.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - IsPaused = info.IsPaused, - PlaySessionId = info.PlaySessionId, - IsAutomated = isAutomated, - Session = session - }); + var eventArgs = new PlaybackProgressEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = session.PlayState.PositionTicks, + MediaSourceId = session.PlayState.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + IsPaused = info.IsPaused, + PlaySessionId = info.PlaySessionId, + IsAutomated = isAutomated, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + + PlaybackProgress?.Invoke(this, eventArgs); if (!isAutomated) { @@ -943,23 +948,23 @@ namespace Emby.Server.Implementations.Session } } - EventHelper.QueueEventIfNotNull( - PlaybackStopped, - this, - new PlaybackStopEventArgs - { - Item = libraryItem, - Users = users, - PlaybackPositionTicks = info.PositionTicks, - PlayedToCompletion = playedToCompletion, - MediaSourceId = info.MediaSourceId, - MediaInfo = info.Item, - DeviceName = session.DeviceName, - ClientName = session.Client, - DeviceId = session.DeviceId, - Session = session - }, - _logger); + var eventArgs = new PlaybackStopEventArgs + { + Item = libraryItem, + Users = users, + PlaybackPositionTicks = info.PositionTicks, + PlayedToCompletion = playedToCompletion, + MediaSourceId = info.MediaSourceId, + MediaInfo = info.Item, + DeviceName = session.DeviceName, + ClientName = session.Client, + DeviceId = session.DeviceId, + Session = session + }; + + await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false); + + EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger); } private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) @@ -1032,7 +1037,7 @@ namespace Emby.Server.Implementations.Session var generalCommand = new GeneralCommand { - Name = GeneralCommandType.DisplayMessage.ToString() + Name = GeneralCommandType.DisplayMessage }; generalCommand.Arguments["Header"] = command.Header; @@ -1059,10 +1064,10 @@ namespace Emby.Server.Implementations.Session AssertCanControl(session, controllingSession); } - return SendMessageToSession(session, "GeneralCommand", command, cancellationToken); + return SendMessageToSession(session, SessionMessageType.GeneralCommand, command, cancellationToken); } - private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken) + private static async Task SendMessageToSession<T>(SessionInfo session, SessionMessageType name, T data, CancellationToken cancellationToken) { var controllers = session.SessionControllers; var messageId = Guid.NewGuid(); @@ -1073,7 +1078,7 @@ namespace Emby.Server.Implementations.Session } } - private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, string name, T data, CancellationToken cancellationToken) + private static Task SendMessageToSessions<T>(IEnumerable<SessionInfo> sessions, SessionMessageType name, T data, CancellationToken cancellationToken) { IEnumerable<Task> GetTasks() { @@ -1173,7 +1178,7 @@ namespace Emby.Server.Implementations.Session } } - await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.Play, command, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -1181,7 +1186,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.SyncPlayCommand, command, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> @@ -1189,7 +1194,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); var session = GetSessionToRemoteControl(sessionId); - await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false); + await SendMessageToSession(session, SessionMessageType.SyncPlayGroupUpdate, command, cancellationToken).ConfigureAwait(false); } private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user) @@ -1263,7 +1268,7 @@ namespace Emby.Server.Implementations.Session { var generalCommand = new GeneralCommand { - Name = GeneralCommandType.DisplayContent.ToString(), + Name = GeneralCommandType.DisplayContent, Arguments = { ["ItemId"] = command.ItemId, @@ -1292,7 +1297,7 @@ namespace Emby.Server.Implementations.Session } } - return SendMessageToSession(session, "Playstate", command, cancellationToken); + return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken); } private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession) @@ -1317,7 +1322,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - return SendMessageToSessions(Sessions, "RestartRequired", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken); } /// <summary> @@ -1329,7 +1334,7 @@ namespace Emby.Server.Implementations.Session { CheckDisposed(); - return SendMessageToSessions(Sessions, "ServerShuttingDown", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken); } /// <summary> @@ -1343,7 +1348,7 @@ namespace Emby.Server.Implementations.Session _logger.LogDebug("Beginning SendServerRestartNotification"); - return SendMessageToSessions(Sessions, "ServerRestarting", string.Empty, cancellationToken); + return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken); } /// <summary> @@ -1424,6 +1429,24 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } + public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token) + { + var result = _authRepo.Get(new AuthenticationInfoQuery() + { + AccessToken = token, + DeviceId = _appHost.SystemId, + Limit = 1 + }); + + if (result.TotalRecordCount == 0) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = result.Items[0].UserId; + return AuthenticateNewSessionInternal(request, false); + } + private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); @@ -1461,6 +1484,14 @@ namespace Emby.Server.Implementations.Session throw new SecurityException("User is not allowed access from this device."); } + int sessionsCount = Sessions.Count(i => i.UserId.Equals(user.Id)); + int maxActiveSessions = user.MaxActiveSessions; + _logger.LogInformation("Current/Max sessions for user {User}: {Sessions}/{Max}", user.Username, sessionsCount, maxActiveSessions); + if (maxActiveSessions >= 1 && sessionsCount >= maxActiveSessions) + { + throw new SecurityException("User is at their maximum number of sessions."); + } + var token = GetAuthorizationToken(user, request.DeviceId, request.App, request.AppVersion, request.DeviceName); var session = LogSessionActivity( @@ -1843,7 +1874,7 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public Task SendMessageToAdminSessions<T>(string name, T data, CancellationToken cancellationToken) + public Task SendMessageToAdminSessions<T>(SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1856,7 +1887,7 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, Func<T> dataFn, CancellationToken cancellationToken) + public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, Func<T> dataFn, CancellationToken cancellationToken) { CheckDisposed(); @@ -1871,7 +1902,7 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public Task SendMessageToUserSessions<T>(List<Guid> userIds, string name, T data, CancellationToken cancellationToken) + public Task SendMessageToUserSessions<T>(List<Guid> userIds, SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); @@ -1880,7 +1911,7 @@ namespace Emby.Server.Implementations.Session } /// <inheritdoc /> - public Task SendMessageToUserDeviceSessions<T>(string deviceId, string name, T data, CancellationToken cancellationToken) + public Task SendMessageToUserDeviceSessions<T>(string deviceId, SessionMessageType name, T data, CancellationToken cancellationToken) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 8bebd37dc..a5f847953 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -4,10 +4,11 @@ using System.Linq; using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Events; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session private readonly ILogger<SessionWebSocketListener> _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IHttpServer _httpServer; + private readonly IWebSocketManager _webSocketManager; /// <summary> /// The KeepAlive cancellation token. @@ -72,19 +73,19 @@ namespace Emby.Server.Implementations.Session /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> /// <param name="loggerFactory">The logger factory.</param> - /// <param name="httpServer">The HTTP server.</param> + /// <param name="webSocketManager">The HTTP server.</param> public SessionWebSocketListener( ILogger<SessionWebSocketListener> logger, ISessionManager sessionManager, ILoggerFactory loggerFactory, - IHttpServer httpServer) + IWebSocketManager webSocketManager) { _logger = logger; _sessionManager = sessionManager; _loggerFactory = loggerFactory; - _httpServer = httpServer; + _webSocketManager = webSocketManager; - httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; + webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected; } private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) @@ -121,7 +122,7 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public void Dispose() { - _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected; StopKeepAlive(); } @@ -316,7 +317,7 @@ namespace Emby.Server.Implementations.Session return webSocket.SendAsync( new WebSocketMessage<int> { - MessageType = "ForceKeepAlive", + MessageType = SessionMessageType.ForceKeepAlive, Data = WebSocketLostTimeout }, CancellationToken.None); diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs index 94604ca1e..b986ffa1c 100644 --- a/Emby.Server.Implementations/Session/WebSocketController.cs +++ b/Emby.Server.Implementations/Session/WebSocketController.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Session @@ -65,7 +66,7 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public Task SendMessage<T>( - string name, + SessionMessageType name, Guid messageId, T data, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index ae1a8d0b7..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,248 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Mime; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpRequest : IHttpRequest - { - private const string FormUrlEncoded = "application/x-www-form-urlencoded"; - private const string MultiPartFormData = "multipart/form-data"; - private const string Soap11 = "text/xml; charset=utf-8"; - - private string _remoteIp; - private Dictionary<string, object> _items; - private string _responseContentType; - - public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName) - { - this.OperationName = operationName; - this.Request = httpRequest; - this.Response = httpResponse; - } - - public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString(); - - public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString(); - - public HttpRequest Request { get; } - - public HttpResponse Response { get; } - - public string OperationName { get; set; } - - public string RawUrl => Request.GetEncodedPathAndQuery(); - - public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/'); - - public string RemoteIp - { - get - { - if (_remoteIp != null) - { - return _remoteIp; - } - - IPAddress ip; - - // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip - // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) - { - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) - { - ip = Request.HttpContext.Connection.RemoteIpAddress; - - // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) - ip ??= IPAddress.Loopback; - } - } - - return _remoteIp = NormalizeIp(ip).ToString(); - } - } - - public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - - public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>()); - - public string ResponseContentType - { - get => - _responseContentType - ?? (_responseContentType = GetResponseContentType(Request)); - set => _responseContentType = value; - } - - public string PathInfo => Request.Path.Value; - - public string UserAgent => Request.Headers[HeaderNames.UserAgent]; - - public IHeaderDictionary Headers => Request.Headers; - - public IQueryCollection QueryString => Request.Query; - - public bool IsLocal => - (Request.HttpContext.Connection.LocalIpAddress == null - && Request.HttpContext.Connection.RemoteIpAddress == null) - || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); - - public string HttpMethod => Request.Method; - - public string Verb => HttpMethod; - - public string ContentType => Request.ContentType; - - public Uri UrlReferrer => Request.GetTypedHeaders().Referer; - - public Stream InputStream => Request.Body; - - public long ContentLength => Request.ContentLength ?? 0; - - private string GetHeader(string name) => Request.Headers[name].ToString(); - - private static IPAddress NormalizeIp(IPAddress ip) - { - if (ip.IsIPv4MappedToIPv6) - { - return ip.MapToIPv4(); - } - - return ip; - } - - public static string GetResponseContentType(HttpRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) - { - return specifiedContentType; - } - - const string ServerDefaultContentType = MediaTypeNames.Application.Json; - - var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - string defaultContentType = null; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = ServerDefaultContentType; - } - - var acceptsAnything = false; - var hasDefaultContentType = defaultContentType != null; - if (acceptContentTypes != null) - { - foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes) - { - ReadOnlySpan<char> contentType = acceptsType; - var index = contentType.IndexOf(';'); - if (index != -1) - { - contentType = contentType.Slice(0, index); - } - - contentType = contentType.Trim(); - acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); - - if (acceptsAnything) - { - break; - } - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - { - return defaultContentType; - } - else - { - return ServerDefaultContentType; - } - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - // We could also send a '406 Not Acceptable', but this is allowed also - return ServerDefaultContentType; - } - - public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) - { - return false; - } - - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) - { - return true; - } - } - - return false; - } - - public static bool IsContentType(HttpRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - private static string GetQueryStringContentType(HttpRequest httpReq) - { - ReadOnlySpan<char> format = httpReq.Query["format"].ToString(); - if (format == ReadOnlySpan<char>.Empty) - { - const int FormatMaxLength = 4; - ReadOnlySpan<char> pi = httpReq.Path.ToString(); - if (pi == null || pi.Length <= FormatMaxLength) - { - return null; - } - - if (pi[0] == '/') - { - pi = pi.Slice(1); - } - - format = pi.LeftPart('/'); - if (format.Length > FormatMaxLength) - { - return null; - } - } - - format = format.LeftPart('.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) - { - return "application/xml"; - } - - return null; - } - } -} diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index 80b977731..538479512 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -301,8 +301,7 @@ namespace Emby.Server.Implementations.SyncPlay if (_group.IsPaused) { // Pick a suitable time that accounts for latency - var delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + var delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); // Unpause group and set starting point in future // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position) @@ -452,8 +451,7 @@ namespace Emby.Server.Implementations.SyncPlay else { // Client, that was buffering, resumed playback but did not update others in time - delay = _group.GetHighestPing() * 2; - delay = delay < _group.DefaultPing ? _group.DefaultPing : delay; + delay = Math.Max(_group.GetHighestPing() * 2, GroupInfo.DefaultPing); _group.LastActivity = currentTime.AddMilliseconds( delay); @@ -497,7 +495,7 @@ namespace Emby.Server.Implementations.SyncPlay private void HandlePingUpdateRequest(SessionInfo session, PlaybackRequest request) { // Collected pings are used to account for network latency when unpausing playback - _group.UpdatePing(session, request.Ping ?? _group.DefaultPing); + _group.UpdatePing(session, request.Ping ?? GroupInfo.DefaultPing); } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index d1818deff..ccd1446dd 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.TV .GetItemList( new InternalItemsQuery(user) { - IncludeItemTypes = new[] { typeof(Episode).Name }, + IncludeItemTypes = new[] { nameof(Episode) }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.DatePlayed, SortOrder.Descending) }, SeriesPresentationUniqueKey = presentationUniqueKey, Limit = limit, @@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { typeof(Episode).Name }, + IncludeItemTypes = new[] { nameof(Episode) }, OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Ascending) }, Limit = 1, IsPlayed = false, diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index 73fcbcec3..b7a59cee2 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -68,12 +68,6 @@ namespace Emby.Server.Implementations.Udp { _logger.LogError(ex, "Error sending response message"); } - - var parts = messageText.Split('|'); - if (parts.Length > 1) - { - _appHost.EnableLoopback(parts[1]); - } } else { diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 4f54c06dd..6ead603ae 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -10,17 +10,22 @@ using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Data.Events; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Updates; +using MediaBrowser.Common.System; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Events; +using MediaBrowser.Controller.Events.Updates; using MediaBrowser.Model.IO; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; using Microsoft.Extensions.Logging; +using MediaBrowser.Model.System; namespace Emby.Server.Implementations.Updates { @@ -34,7 +39,8 @@ namespace Emby.Server.Implementations.Updates /// </summary> private readonly ILogger<InstallationManager> _logger; private readonly IApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; + private readonly IEventManager _eventManager; + private readonly IHttpClientFactory _httpClientFactory; private readonly IJsonSerializer _jsonSerializer; private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; @@ -63,24 +69,21 @@ namespace Emby.Server.Implementations.Updates ILogger<InstallationManager> logger, IApplicationHost appHost, IApplicationPaths appPaths, - IHttpClient httpClient, + IEventManager eventManager, + IHttpClientFactory httpClientFactory, IJsonSerializer jsonSerializer, IServerConfigurationManager config, IFileSystem fileSystem, IZipClient zipClient) { - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); _logger = logger; _applicationHost = appHost; _appPaths = appPaths; - _httpClient = httpClient; + _eventManager = eventManager; + _httpClientFactory = httpClientFactory; _jsonSerializer = jsonSerializer; _config = config; _fileSystem = fileSystem; @@ -88,27 +91,6 @@ namespace Emby.Server.Implementations.Updates } /// <inheritdoc /> - public event EventHandler<InstallationInfo> PackageInstalling; - - /// <inheritdoc /> - public event EventHandler<InstallationInfo> PackageInstallationCompleted; - - /// <inheritdoc /> - public event EventHandler<InstallationFailedEventArgs> PackageInstallationFailed; - - /// <inheritdoc /> - public event EventHandler<InstallationInfo> PackageInstallationCancelled; - - /// <inheritdoc /> - public event EventHandler<IPlugin> PluginUninstalled; - - /// <inheritdoc /> - public event EventHandler<InstallationInfo> PluginUpdated; - - /// <inheritdoc /> - public event EventHandler<InstallationInfo> PluginInstalled; - - /// <inheritdoc /> public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal; /// <inheritdoc /> @@ -116,26 +98,18 @@ namespace Emby.Server.Implementations.Updates { try { - using (var response = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = manifest, - CancellationToken = cancellationToken, - CacheMode = CacheMode.Unconditional, - CacheLength = TimeSpan.FromMinutes(3) - }, - HttpMethod.Get).ConfigureAwait(false)) - using (Stream stream = response.Content) + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(manifest, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + + try { - try - { - return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false); - } - catch (SerializationException ex) - { - _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); - return Array.Empty<PackageInfo>(); - } + return await _jsonSerializer.DeserializeFromStreamAsync<IReadOnlyList<PackageInfo>>(stream).ConfigureAwait(false); + } + catch (SerializationException ex) + { + _logger.LogError(ex, "Failed to deserialize the plugin manifest retrieved from {Manifest}", manifest); + return Array.Empty<PackageInfo>(); } } catch (UriFormatException ex) @@ -161,7 +135,12 @@ namespace Emby.Server.Implementations.Updates var result = new List<PackageInfo>(); foreach (RepositoryInfo repository in _config.Configuration.PluginRepositories) { - result.AddRange(await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true)); + foreach (var package in await GetPackages(repository.Url, cancellationToken).ConfigureAwait(true)) + { + package.repositoryName = repository.Name; + package.repositoryUrl = repository.Url; + result.Add(package); + } } return result; @@ -191,7 +170,8 @@ namespace Emby.Server.Implementations.Updates IEnumerable<PackageInfo> availablePackages, string name = null, Guid guid = default, - Version minVersion = null) + Version minVersion = null, + Version specificVersion = null) { var package = FilterPackages(availablePackages, name, guid).FirstOrDefault(); @@ -205,7 +185,11 @@ namespace Emby.Server.Implementations.Updates var availableVersions = package.versions .Where(x => Version.Parse(x.targetAbi) <= appVer); - if (minVersion != null) + if (specificVersion != null) + { + availableVersions = availableVersions.Where(x => new Version(x.version) == specificVersion); + } + else if (minVersion != null) { availableVersions = availableVersions.Where(x => new Version(x.version) >= minVersion); } @@ -235,8 +219,8 @@ namespace Emby.Server.Implementations.Updates { foreach (var plugin in _applicationHost.Plugins) { - var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version); - var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version); + var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); + var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); if (version != null && CompletedInstallations.All(x => x.Guid != version.Guid)) { yield return version; @@ -264,11 +248,11 @@ namespace Emby.Server.Implementations.Updates var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; - PackageInstalling?.Invoke(this, package); + await _eventManager.PublishAsync(new PluginInstallingEventArgs(package)).ConfigureAwait(false); try { - await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); + var isUpdate = await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); lock (_currentInstallationsLock) { @@ -276,8 +260,11 @@ namespace Emby.Server.Implementations.Updates } _completedInstallationsInternal.Add(package); + await _eventManager.PublishAsync(isUpdate + ? (GenericEventArgs<InstallationInfo>)new PluginUpdatedEventArgs(package) + : new PluginInstalledEventArgs(package)).ConfigureAwait(false); - PackageInstallationCompleted?.Invoke(this, package); + _applicationHost.NotifyPendingRestart(); } catch (OperationCanceledException) { @@ -288,7 +275,7 @@ namespace Emby.Server.Implementations.Updates _logger.LogInformation("Package installation cancelled: {0} {1}", package.Name, package.Version); - PackageInstallationCancelled?.Invoke(this, package); + await _eventManager.PublishAsync(new PluginInstallationCancelledEventArgs(package)).ConfigureAwait(false); throw; } @@ -301,11 +288,11 @@ namespace Emby.Server.Implementations.Updates _currentInstallations.Remove(tuple); } - PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs + await _eventManager.PublishAsync(new InstallationFailedEventArgs { InstallationInfo = package, Exception = ex - }); + }).ConfigureAwait(false); throw; } @@ -322,7 +309,7 @@ namespace Emby.Server.Implementations.Updates /// <param name="package">The package.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns><see cref="Task" />.</returns> - private async Task InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) + private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken) { // Set last update time if we were installed before IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => p.Id == package.Guid) @@ -332,20 +319,9 @@ namespace Emby.Server.Implementations.Updates await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); // Do plugin-specific processing - if (plugin == null) - { - _logger.LogInformation("New plugin installed: {0} {1}", package.Name, package.Version); - - PluginInstalled?.Invoke(this, package); - } - else - { - _logger.LogInformation("Plugin updated: {0} {1}", package.Name, package.Version); + _logger.LogInformation(plugin == null ? "New plugin installed: {0} {1}" : "Plugin updated: {0} {1}", package.Name, package.Version); - PluginUpdated?.Invoke(this, package); - } - - _applicationHost.NotifyPendingRestart(); + return plugin != null; } private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken) @@ -360,42 +336,44 @@ namespace Emby.Server.Implementations.Updates // Always override the passed-in target (which is a file) and figure it out again string targetDir = Path.Combine(_appPaths.PluginsPath, package.Name); + using var response = await _httpClientFactory.CreateClient(NamedClient.Default) + .GetAsync(package.SourceUrl, cancellationToken).ConfigureAwait(false); + await using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - using (var res = await _httpClient.SendAsync( - new HttpRequestOptions - { - Url = package.SourceUrl, - CancellationToken = cancellationToken, - // We need it to be buffered for setting the position - BufferContent = true - }, - HttpMethod.Get).ConfigureAwait(false)) - using (var stream = res.Content) - using (var md5 = MD5.Create()) + using var md5 = MD5.Create(); + cancellationToken.ThrowIfCancellationRequested(); + + var hash = Hex.Encode(md5.ComputeHash(stream)); + if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { - cancellationToken.ThrowIfCancellationRequested(); + _logger.LogError( + "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", + package.Name, + package.Checksum, + hash); + throw new InvalidDataException("The checksum of the received data doesn't match."); + } - var hash = Hex.Encode(md5.ComputeHash(stream)); - if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogError( - "The checksums didn't match while installing {Package}, expected: {Expected}, got: {Received}", - package.Name, - package.Checksum, - hash); - throw new InvalidDataException("The checksum of the received data doesn't match."); - } + // Version folder as they cannot be overwritten in Windows. + targetDir += "_" + package.Version; - if (Directory.Exists(targetDir)) + if (Directory.Exists(targetDir)) + { + try { Directory.Delete(targetDir, true); } - - stream.Position = 0; - _zipClient.ExtractAllFromZip(stream, targetDir, true); + catch + { + // Ignore any exceptions. + } } + stream.Position = 0; + _zipClient.ExtractAllFromZip(stream, targetDir, true); + #pragma warning restore CA5351 } @@ -434,15 +412,22 @@ namespace Emby.Server.Implementations.Updates path = file; } - if (isDirectory) + try { - _logger.LogInformation("Deleting plugin directory {0}", path); - Directory.Delete(path, true); + if (isDirectory) + { + _logger.LogInformation("Deleting plugin directory {0}", path); + Directory.Delete(path, true); + } + else + { + _logger.LogInformation("Deleting plugin file {0}", path); + _fileSystem.DeleteFile(path); + } } - else + catch { - _logger.LogInformation("Deleting plugin file {0}", path); - _fileSystem.DeleteFile(path); + // Ignore file errors. } var list = _config.Configuration.UninstalledPlugins.ToList(); @@ -454,7 +439,7 @@ namespace Emby.Server.Implementations.Updates _config.SaveConfiguration(); } - PluginUninstalled?.Invoke(this, plugin); + _eventManager.Publish(new PluginUninstalledEventArgs(plugin)); _applicationHost.NotifyPendingRestart(); } |
