aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations
diff options
context:
space:
mode:
authorConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>2020-05-29 17:42:42 -0500
committerConfusedPolarBear <33811686+ConfusedPolarBear@users.noreply.github.com>2020-05-29 17:42:42 -0500
commit5d281adedd0d36f34dd3cb8344af3e6a44b5a29f (patch)
tree15bafdf4e3338872fb7e33e341282f4c50de0438 /Emby.Server.Implementations
parent70e50dfa90575cc5e906be1509d3ed363eb1ada4 (diff)
parent02624c9df8492b019539f235307108d49774434c (diff)
Merge remote-tracking branch 'upstream/master' into quickconnect
Diffstat (limited to 'Emby.Server.Implementations')
-rw-r--r--Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs336
-rw-r--r--Emby.Server.Implementations/Activity/ActivityManager.cs67
-rw-r--r--Emby.Server.Implementations/Activity/ActivityRepository.cs313
-rw-r--r--Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs5
-rw-r--r--Emby.Server.Implementations/AppBase/ConfigurationHelper.cs26
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs655
-rw-r--r--Emby.Server.Implementations/Archiving/ZipClient.cs130
-rw-r--r--Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs6
-rw-r--r--Emby.Server.Implementations/Browser/BrowserLauncher.cs8
-rw-r--r--Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs15
-rw-r--r--Emby.Server.Implementations/Channels/ChannelImageProvider.cs18
-rw-r--r--Emby.Server.Implementations/Channels/ChannelManager.cs331
-rw-r--r--Emby.Server.Implementations/Channels/ChannelPostScanTask.cs21
-rw-r--r--Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs20
-rw-r--r--Emby.Server.Implementations/Collections/CollectionImageProvider.cs24
-rw-r--r--Emby.Server.Implementations/Collections/CollectionManager.cs57
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs23
-rw-r--r--Emby.Server.Implementations/ConfigurationOptions.cs3
-rw-r--r--Emby.Server.Implementations/Cryptography/CryptographyProvider.cs41
-rw-r--r--Emby.Server.Implementations/Data/SqliteExtensions.cs27
-rw-r--r--Emby.Server.Implementations/Data/SqliteItemRepository.cs20
-rw-r--r--Emby.Server.Implementations/Data/SqliteUserDataRepository.cs10
-rw-r--r--Emby.Server.Implementations/Devices/DeviceManager.cs292
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs30
-rw-r--r--Emby.Server.Implementations/Emby.Server.Implementations.csproj17
-rw-r--r--Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs95
-rw-r--r--Emby.Server.Implementations/EntryPoints/StartupWizard.cs45
-rw-r--r--Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs9
-rw-r--r--Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs9
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpListenerHost.cs333
-rw-r--r--Emby.Server.Implementations/HttpServer/HttpResultFactory.cs14
-rw-r--r--Emby.Server.Implementations/HttpServer/IHttpListener.cs39
-rw-r--r--Emby.Server.Implementations/HttpServer/ResponseFilter.cs23
-rw-r--r--Emby.Server.Implementations/HttpServer/Security/AuthService.cs37
-rw-r--r--Emby.Server.Implementations/HttpServer/WebSocketConnection.cs267
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs98
-rw-r--r--Emby.Server.Implementations/IStartupOptions.cs7
-rw-r--r--Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs47
-rw-r--r--Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs2
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs74
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs231
-rw-r--r--Emby.Server.Implementations/Library/MediaSourceManager.cs22
-rw-r--r--Emby.Server.Implementations/Library/MediaStreamSelector.cs6
-rw-r--r--Emby.Server.Implementations/Library/PathExtensions.cs24
-rw-r--r--Emby.Server.Implementations/Library/ResolverHelper.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/SearchEngine.cs7
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs37
-rw-r--r--Emby.Server.Implementations/Library/UserManager.cs46
-rw-r--r--Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs26
-rw-r--r--Emby.Server.Implementations/LiveTv/LiveTvManager.cs47
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs2
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs23
-rw-r--r--Emby.Server.Implementations/Localization/Core/af.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ar.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/bn.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ca.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/de.json6
-rw-r--r--Emby.Server.Implementations/Localization/Core/el.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-AR.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/es-MX.json72
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fa.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/fi.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr-CA.json21
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/gsw.json92
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json28
-rw-r--r--Emby.Server.Implementations/Localization/Core/hr.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/it.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/ja.json11
-rw-r--r--Emby.Server.Implementations/Localization/Core/lt-LT.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/mk.json9
-rw-r--r--Emby.Server.Implementations/Localization/Core/nb.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/nl.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/pl.json24
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-BR.json8
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt-PT.json26
-rw-r--r--Emby.Server.Implementations/Localization/Core/pt.json10
-rw-r--r--Emby.Server.Implementations/Localization/Core/ru.json66
-rw-r--r--Emby.Server.Implementations/Localization/Core/sl-SI.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/sv.json7
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json71
-rw-r--r--Emby.Server.Implementations/Localization/Core/tr.json23
-rw-r--r--Emby.Server.Implementations/Localization/Core/uk.json36
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-HK.json59
-rw-r--r--Emby.Server.Implementations/Localization/Core/zh-TW.json36
-rw-r--r--Emby.Server.Implementations/Localization/LocalizationManager.cs3
-rw-r--r--Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs39
-rw-r--r--Emby.Server.Implementations/Net/IWebSocket.cs48
-rw-r--r--Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs29
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs20
-rw-r--r--Emby.Server.Implementations/Security/AuthenticationRepository.cs4
-rw-r--r--Emby.Server.Implementations/ServerApplicationPaths.cs12
-rw-r--r--Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs16
-rw-r--r--Emby.Server.Implementations/Services/UrlExtensions.cs20
-rw-r--r--Emby.Server.Implementations/Session/HttpSessionController.cs191
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs34
-rw-r--r--Emby.Server.Implementations/Session/SessionWebSocketListener.cs275
-rw-r--r--Emby.Server.Implementations/Session/WebSocketController.cs86
-rw-r--r--Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs105
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs135
-rw-r--r--Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs24
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayController.cs517
-rw-r--r--Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs398
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs16
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs77
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketHandler.cs10
-rw-r--r--Emby.Server.Implementations/WebSockets/WebSocketManager.cs102
115 files changed, 3531 insertions, 3554 deletions
diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
index d900520b2..3983824a3 100644
--- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
+++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs
@@ -1,16 +1,13 @@
-#pragma warning disable CS1591
-
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.Devices;
-using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
@@ -27,9 +24,12 @@ 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 _logger;
+ private readonly ILogger<ActivityLogEntryPoint> _logger;
private readonly IInstallationManager _installationManager;
private readonly ISessionManager _sessionManager;
private readonly ITaskManager _taskManager;
@@ -37,25 +37,21 @@ namespace Emby.Server.Implementations.Activity
private readonly ILocalizationManager _localization;
private readonly ISubtitleManager _subManager;
private readonly IUserManager _userManager;
- private readonly IDeviceManager _deviceManager;
/// <summary>
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
/// </summary>
- /// <param name="logger"></param>
- /// <param name="sessionManager"></param>
- /// <param name="deviceManager"></param>
- /// <param name="taskManager"></param>
- /// <param name="activityManager"></param>
- /// <param name="localization"></param>
- /// <param name="installationManager"></param>
- /// <param name="subManager"></param>
- /// <param name="userManager"></param>
- /// <param name="appHost"></param>
+ /// <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,
- IDeviceManager deviceManager,
ITaskManager taskManager,
IActivityManager activityManager,
ILocalizationManager localization,
@@ -65,7 +61,6 @@ namespace Emby.Server.Implementations.Activity
{
_logger = logger;
_sessionManager = sessionManager;
- _deviceManager = deviceManager;
_taskManager = taskManager;
_activityManager = activityManager;
_localization = localization;
@@ -74,6 +69,7 @@ namespace Emby.Server.Implementations.Activity
_userManager = userManager;
}
+ /// <inheritdoc />
public Task RunAsync()
{
_taskManager.TaskCompleted += OnTaskCompleted;
@@ -98,52 +94,38 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += OnUserLockedOut;
- _deviceManager.CameraImageUploaded += OnCameraImageUploaded;
-
return Task.CompletedTask;
}
- private void OnCameraImageUploaded(object sender, GenericEventArgs<CameraImageUploadInfo> e)
+ private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("CameraImageUploadedFrom"),
- e.Argument.Device.Name),
- Type = NotificationType.CameraImageUploaded.ToString()
- });
- }
-
- private void OnUserLockedOut(object sender, GenericEventArgs<User> e)
- {
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("UserLockedOutWithName"),
- e.Argument.Name),
- Type = NotificationType.UserLockedOut.ToString(),
- UserId = e.Argument.Id
- });
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("UserLockedOutWithName"),
+ e.Argument.Name),
+ NotificationType.UserLockedOut.ToString(),
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
+ private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
e.Provider,
- Emby.Notifications.NotificationEntryPoint.GetItemName(e.Item)),
- Type = "SubtitleDownloadFailure",
+ Notifications.NotificationEntryPoint.GetItemName(e.Item)),
+ "SubtitleDownloadFailure",
+ Guid.Empty)
+ {
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
ShortOverview = e.Exception.Message
- });
+ }).ConfigureAwait(false);
}
- private void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
+ private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
{
var item = e.MediaInfo;
@@ -166,15 +148,19 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users[0];
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"), user.Name, GetItemName(item), e.DeviceName),
- Type = GetPlaybackStoppedNotificationType(item.MediaType),
- UserId = user.Id
- });
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ _localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
+ user.Name,
+ GetItemName(item),
+ e.DeviceName),
+ GetPlaybackStoppedNotificationType(item.MediaType),
+ user.Id))
+ .ConfigureAwait(false);
}
- private void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
+ private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
{
var item = e.MediaInfo;
@@ -197,17 +183,16 @@ namespace Emby.Server.Implementations.Activity
var user = e.Users.First();
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Name,
GetItemName(item),
e.DeviceName),
- Type = GetPlaybackNotificationType(item.MediaType),
- UserId = user.Id
- });
+ GetPlaybackNotificationType(item.MediaType),
+ user.Id))
+ .ConfigureAwait(false);
}
private static string GetItemName(BaseItemDto item)
@@ -257,236 +242,215 @@ namespace Emby.Server.Implementations.Activity
return null;
}
- private void OnSessionEnded(object sender, SessionEventArgs e)
+ private async void OnSessionEnded(object sender, SessionEventArgs e)
{
- string name;
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
- name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("DeviceOfflineWithName"),
- session.DeviceName);
-
- // Causing too much spam for now
return;
}
- else
- {
- name = string.Format(
+
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOfflineFromDevice"),
session.UserName,
- session.DeviceName);
- }
-
- CreateLogEntry(new ActivityLogEntry
+ session.DeviceName),
+ "SessionEnded",
+ session.UserId)
{
- Name = name,
- Type = "SessionEnded",
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
session.RemoteEndPoint),
- UserId = session.UserId
- });
+ }).ConfigureAwait(false);
}
- private void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
+ private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
{
var user = e.Argument.User;
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
user.Name),
- Type = "AuthenticationSucceeded",
+ "AuthenticationSucceeded",
+ user.Id)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.SessionInfo.RemoteEndPoint),
- UserId = user.Id
- });
+ }).ConfigureAwait(false);
}
- private void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
+ private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
e.Argument.Username),
- Type = "AuthenticationFailed",
+ "AuthenticationFailed",
+ Guid.Empty)
+ {
+ LogSeverity = LogLevel.Error,
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
e.Argument.RemoteEndPoint),
- Severity = LogLevel.Error
- });
+ }).ConfigureAwait(false);
}
- private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
+ private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
e.Argument.Name),
- Type = "UserPolicyUpdated",
- UserId = e.Argument.Id
- });
+ "UserPolicyUpdated",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnUserDeleted(object sender, GenericEventArgs<User> e)
+ private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Name),
- Type = "UserDeleted"
- });
+ "UserDeleted",
+ Guid.Empty))
+ .ConfigureAwait(false);
}
- private void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
+ private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Name),
- Type = "UserPasswordChanged",
- UserId = e.Argument.Id
- });
+ "UserPasswordChanged",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnUserCreated(object sender, GenericEventArgs<User> e)
+ private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Name),
- Type = "UserCreated",
- UserId = e.Argument.Id
- });
+ "UserCreated",
+ e.Argument.Id))
+ .ConfigureAwait(false);
}
- private void OnSessionStarted(object sender, SessionEventArgs e)
+ private async void OnSessionStarted(object sender, SessionEventArgs e)
{
- string name;
var session = e.SessionInfo;
if (string.IsNullOrEmpty(session.UserName))
{
- name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("DeviceOnlineWithName"),
- session.DeviceName);
-
- // Causing too much spam for now
return;
}
- else
- {
- name = string.Format(
+
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserOnlineFromDevice"),
session.UserName,
- session.DeviceName);
- }
-
- CreateLogEntry(new ActivityLogEntry
+ session.DeviceName),
+ "SessionStarted",
+ session.UserId)
{
- Name = name,
- Type = "SessionStarted",
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("LabelIpAddressValue"),
- session.RemoteEndPoint),
- UserId = session.UserId
- });
+ session.RemoteEndPoint)
+ }).ConfigureAwait(false);
}
- private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e)
+ private async void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUpdatedWithName"),
e.Argument.Item1.Name),
- Type = NotificationType.PluginUpdateInstalled.ToString(),
+ NotificationType.PluginUpdateInstalled.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
- e.Argument.Item2.versionStr),
- Overview = e.Argument.Item2.description
- });
+ e.Argument.Item2.version),
+ Overview = e.Argument.Item2.changelog
+ }).ConfigureAwait(false);
}
- private void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
+ private async void OnPluginUninstalled(object sender, GenericEventArgs<IPlugin> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginUninstalledWithName"),
e.Argument.Name),
- Type = NotificationType.PluginUninstalled.ToString()
- });
+ NotificationType.PluginUninstalled.ToString(),
+ Guid.Empty))
+ .ConfigureAwait(false);
}
- private void OnPluginInstalled(object sender, GenericEventArgs<PackageVersionInfo> e)
+ private async void OnPluginInstalled(object sender, GenericEventArgs<VersionInfo> e)
{
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("PluginInstalledWithName"),
e.Argument.name),
- Type = NotificationType.PluginInstalled.ToString(),
+ NotificationType.PluginInstalled.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
- e.Argument.versionStr)
- });
+ e.Argument.version)
+ }).ConfigureAwait(false);
}
- private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
+ private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
{
var installationInfo = e.InstallationInfo;
- CreateLogEntry(new ActivityLogEntry
- {
- Name = string.Format(
+ await CreateLogEntry(new ActivityLog(
+ string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("NameInstallFailed"),
installationInfo.Name),
- Type = NotificationType.InstallationFailed.ToString(),
+ NotificationType.InstallationFailed.ToString(),
+ Guid.Empty)
+ {
ShortOverview = string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("VersionNumber"),
installationInfo.Version),
Overview = e.Exception.Message
- });
+ }).ConfigureAwait(false);
}
- private void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
+ private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
{
var result = e.Result;
var task = e.Task;
- var activityTask = task.ScheduledTask as IConfigurableScheduledTask;
- if (activityTask != null && !activityTask.IsLogged)
+ if (task.ScheduledTask is IConfigurableScheduledTask activityTask
+ && !activityTask.IsLogged)
{
return;
}
@@ -511,22 +475,20 @@ namespace Emby.Server.Implementations.Activity
vals.Add(e.Result.LongErrorMessage);
}
- CreateLogEntry(new ActivityLogEntry
+ await CreateLogEntry(new ActivityLog(
+ string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
+ NotificationType.TaskFailed.ToString(),
+ Guid.Empty)
{
- Name = string.Format(
- CultureInfo.InvariantCulture,
- _localization.GetLocalizedString("ScheduledTaskFailedWithName"),
- task.Name),
- Type = NotificationType.TaskFailed.ToString(),
+ LogSeverity = LogLevel.Error,
Overview = string.Join(Environment.NewLine, vals),
- ShortOverview = runningTime,
- Severity = LogLevel.Error
- });
+ ShortOverview = runningTime
+ }).ConfigureAwait(false);
}
}
- private void CreateLogEntry(ActivityLogEntry entry)
- => _activityManager.Create(entry);
+ private async Task CreateLogEntry(ActivityLog entry)
+ => await _activityManager.CreateAsync(entry).ConfigureAwait(false);
/// <inheritdoc />
public void Dispose()
@@ -553,14 +515,12 @@ namespace Emby.Server.Implementations.Activity
_userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= OnUserLockedOut;
-
- _deviceManager.CameraImageUploaded -= OnCameraImageUploaded;
}
/// <summary>
/// Constructs a user-friendly string for this TimeSpan instance.
/// </summary>
- public static string ToUserFriendlyString(TimeSpan span)
+ private static string ToUserFriendlyString(TimeSpan span)
{
const int DaysInYear = 365;
const int DaysInMonth = 30;
@@ -574,7 +534,7 @@ namespace Emby.Server.Implementations.Activity
{
int years = days / DaysInYear;
values.Add(CreateValueString(years, "year"));
- days = days % DaysInYear;
+ days %= DaysInYear;
}
// Number of months
diff --git a/Emby.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs
deleted file mode 100644
index ee10845cf..000000000
--- a/Emby.Server.Implementations/Activity/ActivityManager.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.Activity
-{
- public class ActivityManager : IActivityManager
- {
- public event EventHandler<GenericEventArgs<ActivityLogEntry>> EntryCreated;
-
- private readonly IActivityRepository _repo;
- private readonly ILogger _logger;
- private readonly IUserManager _userManager;
-
- public ActivityManager(
- ILoggerFactory loggerFactory,
- IActivityRepository repo,
- IUserManager userManager)
- {
- _logger = loggerFactory.CreateLogger(nameof(ActivityManager));
- _repo = repo;
- _userManager = userManager;
- }
-
- public void Create(ActivityLogEntry entry)
- {
- entry.Date = DateTime.UtcNow;
-
- _repo.Create(entry);
-
- EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(entry));
- }
-
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
- {
- var result = _repo.GetActivityLogEntries(minDate, hasUserId, startIndex, limit);
-
- foreach (var item in result.Items)
- {
- if (item.UserId == Guid.Empty)
- {
- continue;
- }
-
- var user = _userManager.GetUserById(item.UserId);
-
- if (user != null)
- {
- var dto = _userManager.GetUserDto(user);
- item.UserPrimaryImageTag = dto.PrimaryImageTag;
- }
- }
-
- return result;
- }
-
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit)
- {
- return GetActivityLogEntries(minDate, null, startIndex, limit);
- }
- }
-}
diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs
deleted file mode 100644
index 7be72319e..000000000
--- a/Emby.Server.Implementations/Activity/ActivityRepository.cs
+++ /dev/null
@@ -1,313 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
-using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Querying;
-using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Activity
-{
- public class ActivityRepository : BaseSqliteRepository, IActivityRepository
- {
- private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
- private readonly IFileSystem _fileSystem;
-
- public ActivityRepository(ILoggerFactory loggerFactory, IServerApplicationPaths appPaths, IFileSystem fileSystem)
- : base(loggerFactory.CreateLogger(nameof(ActivityRepository)))
- {
- DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db");
- _fileSystem = fileSystem;
- }
-
- public void Initialize()
- {
- try
- {
- InitializeInternal();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error loading database file. Will reset and retry.");
-
- _fileSystem.DeleteFile(DbFilePath);
-
- InitializeInternal();
- }
- }
-
- private void InitializeInternal()
- {
- using (var connection = GetConnection())
- {
- connection.RunQueries(new[]
- {
- "create table if not exists ActivityLog (Id INTEGER PRIMARY KEY, Name TEXT NOT NULL, Overview TEXT, ShortOverview TEXT, Type TEXT NOT NULL, ItemId TEXT, UserId TEXT, DateCreated DATETIME NOT NULL, LogSeverity TEXT NOT NULL)",
- "drop index if exists idx_ActivityLogEntries"
- });
-
- TryMigrate(connection);
- }
- }
-
- private void TryMigrate(ManagedConnection connection)
- {
- try
- {
- if (TableExists(connection, "ActivityLogEntries"))
- {
- connection.RunQueries(new[]
- {
- "INSERT INTO ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) SELECT Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity FROM ActivityLogEntries",
- "drop table if exists ActivityLogEntries"
- });
- }
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error migrating activity log database");
- }
- }
-
- private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLog";
-
- public void Create(ActivityLogEntry entry)
- {
- if (entry == null)
- {
- throw new ArgumentNullException(nameof(entry));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("insert into ActivityLog (Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)"))
- {
- statement.TryBind("@Name", entry.Name);
-
- statement.TryBind("@Overview", entry.Overview);
- statement.TryBind("@ShortOverview", entry.ShortOverview);
- statement.TryBind("@Type", entry.Type);
- statement.TryBind("@ItemId", entry.ItemId);
-
- if (entry.UserId.Equals(Guid.Empty))
- {
- statement.TryBindNull("@UserId");
- }
- else
- {
- statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
- statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- public void Update(ActivityLogEntry entry)
- {
- if (entry == null)
- {
- throw new ArgumentNullException(nameof(entry));
- }
-
- using (var connection = GetConnection())
- {
- connection.RunInTransaction(db =>
- {
- using (var statement = db.PrepareStatement("Update ActivityLog set Name=@Name,Overview=@Overview,ShortOverview=@ShortOverview,Type=@Type,ItemId=@ItemId,UserId=@UserId,DateCreated=@DateCreated,LogSeverity=@LogSeverity where Id=@Id"))
- {
- statement.TryBind("@Id", entry.Id);
-
- statement.TryBind("@Name", entry.Name);
- statement.TryBind("@Overview", entry.Overview);
- statement.TryBind("@ShortOverview", entry.ShortOverview);
- statement.TryBind("@Type", entry.Type);
- statement.TryBind("@ItemId", entry.ItemId);
-
- if (entry.UserId.Equals(Guid.Empty))
- {
- statement.TryBindNull("@UserId");
- }
- else
- {
- statement.TryBind("@UserId", entry.UserId.ToString("N", CultureInfo.InvariantCulture));
- }
-
- statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue());
- statement.TryBind("@LogSeverity", entry.Severity.ToString());
-
- statement.MoveNext();
- }
- }, TransactionMode);
- }
- }
-
- public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, bool? hasUserId, int? startIndex, int? limit)
- {
- var commandText = BaseActivitySelectText;
- var whereClauses = new List<string>();
-
- if (minDate.HasValue)
- {
- whereClauses.Add("DateCreated>=@DateCreated");
- }
- if (hasUserId.HasValue)
- {
- if (hasUserId.Value)
- {
- whereClauses.Add("UserId not null");
- }
- else
- {
- whereClauses.Add("UserId is null");
- }
- }
-
- var whereTextWithoutPaging = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- if (startIndex.HasValue && startIndex.Value > 0)
- {
- var pagingWhereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- whereClauses.Add(
- string.Format(
- CultureInfo.InvariantCulture,
- "Id NOT IN (SELECT Id FROM ActivityLog {0} ORDER BY DateCreated DESC LIMIT {1})",
- pagingWhereText,
- startIndex.Value));
- }
-
- var whereText = whereClauses.Count == 0 ?
- string.Empty :
- " where " + string.Join(" AND ", whereClauses.ToArray());
-
- commandText += whereText;
-
- commandText += " ORDER BY DateCreated DESC";
-
- if (limit.HasValue)
- {
- commandText += " LIMIT " + limit.Value.ToString(_usCulture);
- }
-
- var statementTexts = new[]
- {
- commandText,
- "select count (Id) from ActivityLog" + whereTextWithoutPaging
- };
-
- var list = new List<ActivityLogEntry>();
- var result = new QueryResult<ActivityLogEntry>();
-
- using (var connection = GetConnection(true))
- {
- connection.RunInTransaction(
- db =>
- {
- var statements = PrepareAll(db, statementTexts).ToList();
-
- using (var statement = statements[0])
- {
- if (minDate.HasValue)
- {
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
-
- foreach (var row in statement.ExecuteQuery())
- {
- list.Add(GetEntry(row));
- }
- }
-
- using (var statement = statements[1])
- {
- if (minDate.HasValue)
- {
- statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue());
- }
-
- result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First();
- }
- },
- ReadTransactionMode);
- }
-
- result.Items = list;
- return result;
- }
-
- private static ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader)
- {
- var index = 0;
-
- var info = new ActivityLogEntry
- {
- Id = reader[index].ToInt64()
- };
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Name = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Overview = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.ShortOverview = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Type = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.ItemId = reader[index].ToString();
- }
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.UserId = new Guid(reader[index].ToString());
- }
-
- index++;
- info.Date = reader[index].ReadDateTime();
-
- index++;
- if (reader[index].SQLiteType != SQLiteType.Null)
- {
- info.Severity = (LogLevel)Enum.Parse(typeof(LogLevel), reader[index].ToString(), true);
- }
-
- return info;
- }
- }
-}
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
index bc4781743..2adc1d6c3 100644
--- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
+++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
@@ -15,6 +15,11 @@ namespace Emby.Server.Implementations.AppBase
/// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary>
+ /// <param name="programDataPath">The program data path.</param>
+ /// <param name="logDirectoryPath">The log directory path.</param>
+ /// <param name="configurationDirectoryPath">The configuration directory path.</param>
+ /// <param name="cacheDirectoryPath">The cache directory path.</param>
+ /// <param name="webDirectoryPath">The web directory path.</param>
protected BaseApplicationPaths(
string programDataPath,
string logDirectoryPath,
diff --git a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
index 854d7b4cb..0b681fddf 100644
--- a/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
+++ b/Emby.Server.Implementations/AppBase/ConfigurationHelper.cs
@@ -36,24 +36,22 @@ namespace Emby.Server.Implementations.AppBase
configuration = Activator.CreateInstance(type);
}
- using (var stream = new MemoryStream())
- {
- xmlSerializer.SerializeToStream(configuration, stream);
-
- // Take the object we just got and serialize it back to bytes
- var newBytes = stream.ToArray();
+ using var stream = new MemoryStream();
+ xmlSerializer.SerializeToStream(configuration, stream);
- // If the file didn't exist before, or if something has changed, re-save
- if (buffer == null || !buffer.SequenceEqual(newBytes))
- {
- Directory.CreateDirectory(Path.GetDirectoryName(path));
+ // Take the object we just got and serialize it back to bytes
+ var newBytes = stream.ToArray();
- // Save it after load in case we got new items
- File.WriteAllBytes(path, newBytes);
- }
+ // If the file didn't exist before, or if something has changed, re-save
+ if (buffer == null || !buffer.SequenceEqual(newBytes))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(path));
- return configuration;
+ // Save it after load in case we got new items
+ File.WriteAllBytes(path, newBytes);
}
+
+ return configuration;
}
}
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index de044a4aa..0a349bb33 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -22,7 +22,6 @@ using Emby.Dlna.Ssdp;
using Emby.Drawing;
using Emby.Notifications;
using Emby.Photos;
-using Emby.Server.Implementations.Activity;
using Emby.Server.Implementations.Archiving;
using Emby.Server.Implementations.Channels;
using Emby.Server.Implementations.Collections;
@@ -44,9 +43,9 @@ using Emby.Server.Implementations.Security;
using Emby.Server.Implementations.Serialization;
using Emby.Server.Implementations.Services;
using Emby.Server.Implementations.Session;
-using Emby.Server.Implementations.SocketSharp;
using Emby.Server.Implementations.TV;
using Emby.Server.Implementations.Updates;
+using Emby.Server.Implementations.SyncPlay;
using MediaBrowser.Api;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@@ -81,13 +80,12 @@ using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.TV;
+using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo;
-using MediaBrowser.Model.Activity;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
-using MediaBrowser.Model.Events;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -96,7 +94,6 @@ using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.System;
using MediaBrowser.Model.Tasks;
-using MediaBrowser.Model.Updates;
using MediaBrowser.Providers.Chapters;
using MediaBrowser.Providers.Manager;
using MediaBrowser.Providers.Plugins.TheTvdb;
@@ -104,11 +101,11 @@ using MediaBrowser.Providers.Subtitles;
using MediaBrowser.WebDashboard.Api;
using MediaBrowser.XbmcMetadata.Providers;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Prometheus.DotNetRuntime;
using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
+using Emby.Server.Implementations.QuickConnect;
namespace Emby.Server.Implementations
{
@@ -122,14 +119,20 @@ namespace Emby.Server.Implementations
/// </summary>
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
- private SqliteUserRepository _userRepository;
- private SqliteDisplayPreferencesRepository _displayPreferencesRepository;
+ private readonly IFileSystem _fileSystemManager;
+ private readonly INetworkManager _networkManager;
+ private readonly IXmlSerializer _xmlSerializer;
+ private readonly IStartupOptions _startupOptions;
+
+ private IMediaEncoder _mediaEncoder;
+ private ISessionManager _sessionManager;
+ private IHttpServer _httpServer;
+ private IHttpClient _httpClient;
/// <summary>
/// Gets a value indicating whether this instance can self restart.
/// </summary>
- /// <value><c>true</c> if this instance can self restart; otherwise, <c>false</c>.</value>
- public abstract bool CanSelfRestart { get; }
+ public bool CanSelfRestart => _startupOptions.RestartPath != null;
public virtual bool CanLaunchWebBrowser
{
@@ -140,7 +143,7 @@ namespace Emby.Server.Implementations
return false;
}
- if (StartupOptions.IsService)
+ if (_startupOptions.IsService)
{
return false;
}
@@ -210,21 +213,6 @@ namespace Emby.Server.Implementations
/// <value>The configuration manager.</value>
protected IConfigurationManager ConfigurationManager { get; set; }
- public IFileSystem FileSystemManager { get; set; }
-
- /// <inheritdoc />
- public PackageVersionClass SystemUpdateLevel
- {
- get
- {
-#if BETA
- return PackageVersionClass.Beta;
-#else
- return PackageVersionClass.Release;
-#endif
- }
- }
-
/// <summary>
/// Gets or sets the service provider.
/// </summary>
@@ -247,110 +235,6 @@ namespace Emby.Server.Implementations
public IServerConfigurationManager ServerConfigurationManager => (IServerConfigurationManager)ConfigurationManager;
/// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
-
- /// <summary>
- /// Gets or sets the library manager.
- /// </summary>
- /// <value>The library manager.</value>
- internal ILibraryManager LibraryManager { get; set; }
-
- /// <summary>
- /// Gets or sets the directory watchers.
- /// </summary>
- /// <value>The directory watchers.</value>
- private ILibraryMonitor LibraryMonitor { get; set; }
-
- /// <summary>
- /// Gets or sets the provider manager.
- /// </summary>
- /// <value>The provider manager.</value>
- private IProviderManager ProviderManager { get; set; }
-
- /// <summary>
- /// Gets or sets the HTTP server.
- /// </summary>
- /// <value>The HTTP server.</value>
- private IHttpServer HttpServer { get; set; }
-
- private IDtoService DtoService { get; set; }
-
- public IImageProcessor ImageProcessor { get; set; }
-
- /// <summary>
- /// Gets or sets the media encoder.
- /// </summary>
- /// <value>The media encoder.</value>
- private IMediaEncoder MediaEncoder { get; set; }
-
- private ISubtitleEncoder SubtitleEncoder { get; set; }
-
- private ISessionManager SessionManager { get; set; }
-
- private ILiveTvManager LiveTvManager { get; set; }
-
- public LocalizationManager LocalizationManager { get; set; }
-
- private IEncodingManager EncodingManager { get; set; }
-
- private IChannelManager ChannelManager { get; set; }
-
- /// <summary>
- /// Gets or sets the user data repository.
- /// </summary>
- /// <value>The user data repository.</value>
- private IUserDataManager UserDataManager { get; set; }
-
- internal SqliteItemRepository ItemRepository { get; set; }
-
- private INotificationManager NotificationManager { get; set; }
-
- private ISubtitleManager SubtitleManager { get; set; }
-
- private IChapterManager ChapterManager { get; set; }
-
- private IDeviceManager DeviceManager { get; set; }
-
- internal IUserViewManager UserViewManager { get; set; }
-
- private IAuthenticationRepository AuthenticationRepository { get; set; }
-
- private ITVSeriesManager TVSeriesManager { get; set; }
-
- private ICollectionManager CollectionManager { get; set; }
-
- private IMediaSourceManager MediaSourceManager { get; set; }
-
- /// <summary>
- /// Gets the installation manager.
- /// </summary>
- /// <value>The installation manager.</value>
- protected IInstallationManager InstallationManager { get; private set; }
-
- protected IAuthService AuthService { get; private set; }
-
- public IStartupOptions StartupOptions { get; }
-
- internal IImageEncoder ImageEncoder { get; private set; }
-
- protected readonly IXmlSerializer XmlSerializer;
-
- protected ISocketFactory SocketFactory { get; private set; }
-
- protected ITaskManager TaskManager { get; private set; }
-
- public IHttpClient HttpClient { get; private set; }
-
- protected INetworkManager NetworkManager { get; set; }
-
- public IJsonSerializer JsonSerializer { get; private set; }
-
- protected IIsoManager IsoManager { get; private set; }
-
- /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost" /> class.
/// </summary>
public ApplicationHost(
@@ -358,29 +242,39 @@ namespace Emby.Server.Implementations
ILoggerFactory loggerFactory,
IStartupOptions options,
IFileSystem fileSystem,
- IImageEncoder imageEncoder,
INetworkManager networkManager)
{
- XmlSerializer = new MyXmlSerializer();
+ _xmlSerializer = new MyXmlSerializer();
- NetworkManager = networkManager;
+ _networkManager = networkManager;
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
ApplicationPaths = applicationPaths;
LoggerFactory = loggerFactory;
- FileSystemManager = fileSystem;
+ _fileSystemManager = fileSystem;
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, XmlSerializer, FileSystemManager);
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
- Logger = LoggerFactory.CreateLogger("App");
+ Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- StartupOptions = options;
+ _startupOptions = options;
- ImageEncoder = imageEncoder;
+ // Initialize runtime stat collection
+ if (ServerConfigurationManager.Configuration.EnableMetrics)
+ {
+ DotNetRuntimeStatsBuilder.Default().StartCollecting();
+ }
fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
- NetworkManager.NetworkChanged += OnNetworkChanged;
+ _networkManager.NetworkChanged += OnNetworkChanged;
+
+ CertificateInfo = new CertificateInfo
+ {
+ Path = ServerConfigurationManager.Configuration.CertificatePath,
+ Password = ServerConfigurationManager.Configuration.CertificatePassword
+ };
+ Certificate = GetCertificate(CertificateInfo);
}
public string ExpandVirtualPath(string path)
@@ -448,10 +342,7 @@ namespace Emby.Server.Implementations
}
}
- /// <summary>
- /// Gets the name.
- /// </summary>
- /// <value>The name.</value>
+ /// <inheritdoc/>
public string Name => ApplicationProductName;
/// <summary>
@@ -541,7 +432,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
- MediaEncoder.SetFFmpegPath();
+ _mediaEncoder.SetFFmpegPath();
Logger.LogInformation("ServerId: {0}", SystemId);
@@ -553,7 +444,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;
+ _httpServer.GlobalResponse = null;
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -577,7 +468,7 @@ namespace Emby.Server.Implementations
}
/// <inheritdoc/>
- public async Task InitAsync(IServiceCollection serviceCollection, IConfiguration startupConfig)
+ public void Init(IServiceCollection serviceCollection)
{
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
@@ -589,8 +480,6 @@ namespace Emby.Server.Implementations
HttpsPort = ServerConfiguration.DefaultHttpsPort;
}
- JsonSerializer = new JsonSerializer();
-
if (Plugins != null)
{
var pluginBuilder = new StringBuilder();
@@ -610,41 +499,19 @@ namespace Emby.Server.Implementations
DiscoverTypes();
- await RegisterServices(serviceCollection, startupConfig).ConfigureAwait(false);
+ RegisterServices(serviceCollection);
}
- public async Task ExecuteWebsocketHandlerAsync(HttpContext context, Func<Task> next)
- {
- if (!context.WebSockets.IsWebSocketRequest)
- {
- await next().ConfigureAwait(false);
- return;
- }
-
- await HttpServer.ProcessWebSocketRequest(context).ConfigureAwait(false);
- }
-
- public async Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
- {
- if (context.WebSockets.IsWebSocketRequest)
- {
- await next().ConfigureAwait(false);
- return;
- }
-
- var request = context.Request;
- var response = context.Response;
- var localPath = context.Request.Path.ToString();
-
- var req = new WebSocketSharpRequest(request, response, request.Path, LoggerFactory.CreateLogger<WebSocketSharpRequest>());
- await HttpServer.RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted).ConfigureAwait(false);
- }
+ 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 async Task RegisterServices(IServiceCollection serviceCollection, IConfiguration startupConfig)
+ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
+ serviceCollection.AddSingleton(_startupOptions);
+
serviceCollection.AddMemoryCache();
serviceCollection.AddSingleton(ConfigurationManager);
@@ -652,233 +519,161 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton(JsonSerializer);
-
- // TODO: Support for injecting ILogger should be deprecated in favour of ILogger<T> and this removed
- serviceCollection.AddSingleton<ILogger>(Logger);
+ serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
- serviceCollection.AddSingleton(FileSystemManager);
+ serviceCollection.AddSingleton(_fileSystemManager);
serviceCollection.AddSingleton<TvdbClientManager>();
- HttpClient = new HttpClientManager.HttpClientManager(
- ApplicationPaths,
- LoggerFactory.CreateLogger<HttpClientManager.HttpClientManager>(),
- FileSystemManager,
- () => ApplicationUserAgent);
- serviceCollection.AddSingleton(HttpClient);
+ serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
- serviceCollection.AddSingleton(NetworkManager);
+ serviceCollection.AddSingleton(_networkManager);
- IsoManager = new IsoManager();
- serviceCollection.AddSingleton(IsoManager);
+ serviceCollection.AddSingleton<IIsoManager, IsoManager>();
- TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LoggerFactory, FileSystemManager);
- serviceCollection.AddSingleton(TaskManager);
+ serviceCollection.AddSingleton<ITaskManager, TaskManager>();
- serviceCollection.AddSingleton(XmlSerializer);
+ serviceCollection.AddSingleton(_xmlSerializer);
- serviceCollection.AddSingleton(typeof(IStreamHelper), typeof(StreamHelper));
+ serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
- var cryptoProvider = new CryptographyProvider();
- serviceCollection.AddSingleton<ICryptoProvider>(cryptoProvider);
+ serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
- SocketFactory = new SocketFactory();
- serviceCollection.AddSingleton(SocketFactory);
+ serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
- serviceCollection.AddSingleton(typeof(IInstallationManager), typeof(InstallationManager));
+ serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
- serviceCollection.AddSingleton(typeof(IZipClient), typeof(ZipClient));
+ serviceCollection.AddSingleton<IZipClient, ZipClient>();
- serviceCollection.AddSingleton(typeof(IHttpResultFactory), typeof(HttpResultFactory));
+ serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
serviceCollection.AddSingleton<IServerApplicationHost>(this);
serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(ServerConfigurationManager);
- LocalizationManager = new LocalizationManager(ServerConfigurationManager, JsonSerializer, LoggerFactory.CreateLogger<LocalizationManager>());
- await LocalizationManager.LoadAll().ConfigureAwait(false);
- serviceCollection.AddSingleton<ILocalizationManager>(LocalizationManager);
-
- serviceCollection.AddSingleton<IBlurayExaminer>(new BdInfoExaminer(FileSystemManager));
-
- UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager);
- serviceCollection.AddSingleton(UserDataManager);
+ serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
- _displayPreferencesRepository = new SqliteDisplayPreferencesRepository(
- LoggerFactory.CreateLogger<SqliteDisplayPreferencesRepository>(),
- ApplicationPaths,
- FileSystemManager);
- serviceCollection.AddSingleton<IDisplayPreferencesRepository>(_displayPreferencesRepository);
+ serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
- ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, LoggerFactory.CreateLogger<SqliteItemRepository>(), LocalizationManager);
- serviceCollection.AddSingleton<IItemRepository>(ItemRepository);
+ serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+ serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
- AuthenticationRepository = GetAuthenticationRepository();
- serviceCollection.AddSingleton(AuthenticationRepository);
+ serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
- _userRepository = GetUserRepository();
+ serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
- UserManager = new UserManager(
- LoggerFactory.CreateLogger<UserManager>(),
- _userRepository,
- XmlSerializer,
- NetworkManager,
- () => ImageProcessor,
- () => DtoService,
- this,
- JsonSerializer,
- FileSystemManager,
- cryptoProvider);
+ serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
- serviceCollection.AddSingleton(UserManager);
+ serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
- MediaEncoder = new MediaBrowser.MediaEncoding.Encoder.MediaEncoder(
- LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(),
- ServerConfigurationManager,
- FileSystemManager,
- LocalizationManager,
- () => SubtitleEncoder,
- startupConfig,
- StartupOptions.FFmpegPath);
- serviceCollection.AddSingleton(MediaEncoder);
+ // 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.AddSingleton<IUserManager, UserManager>();
- LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager, MediaEncoder);
- serviceCollection.AddSingleton(LibraryManager);
+ // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
+ // TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
+ serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
+ serviceCollection.AddSingleton<IMediaEncoder>(provider =>
+ ActivatorUtilities.CreateInstance<MediaBrowser.MediaEncoding.Encoder.MediaEncoder>(provider, _startupOptions.FFmpegPath ?? string.Empty));
- var musicManager = new MusicManager(LibraryManager);
- serviceCollection.AddSingleton<IMusicManager>(musicManager);
+ // 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>();
- LibraryMonitor = new LibraryMonitor(LoggerFactory, LibraryManager, ServerConfigurationManager, FileSystemManager);
- serviceCollection.AddSingleton(LibraryMonitor);
+ serviceCollection.AddSingleton<IMusicManager, MusicManager>();
- serviceCollection.AddSingleton<ISearchEngine>(new SearchEngine(LoggerFactory, LibraryManager, UserManager));
+ serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
- CertificateInfo = GetCertificateInfo(true);
- Certificate = GetCertificate(CertificateInfo);
+ serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
serviceCollection.AddSingleton<ServiceController>();
- serviceCollection.AddSingleton<IHttpListener, WebSocketSharpListener>();
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
- ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder);
- serviceCollection.AddSingleton(ImageProcessor);
+ serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
- TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager);
- serviceCollection.AddSingleton(TVSeriesManager);
+ serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
- DeviceManager = new DeviceManager(AuthenticationRepository, JsonSerializer, LibraryManager, LocalizationManager, UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager);
- serviceCollection.AddSingleton(DeviceManager);
+ serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
- MediaSourceManager = new MediaSourceManager(ItemRepository, ApplicationPaths, LocalizationManager, UserManager, LibraryManager, LoggerFactory, JsonSerializer, FileSystemManager, UserDataManager, () => MediaEncoder);
- serviceCollection.AddSingleton(MediaSourceManager);
+ serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
- SubtitleManager = new SubtitleManager(LoggerFactory, FileSystemManager, LibraryMonitor, MediaSourceManager, LocalizationManager);
- serviceCollection.AddSingleton(SubtitleManager);
+ serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
- ProviderManager = new ProviderManager(HttpClient, SubtitleManager, ServerConfigurationManager, LibraryMonitor, LoggerFactory, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer);
- serviceCollection.AddSingleton(ProviderManager);
+ serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
- DtoService = new DtoService(LoggerFactory, LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ProviderManager, this, () => MediaSourceManager, () => LiveTvManager);
- serviceCollection.AddSingleton(DtoService);
+ // 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>();
- ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LoggerFactory, ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, ProviderManager);
- serviceCollection.AddSingleton(ChannelManager);
+ serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
- SessionManager = new SessionManager(
- LoggerFactory.CreateLogger<SessionManager>(),
- UserDataManager,
- LibraryManager,
- UserManager,
- musicManager,
- DtoService,
- ImageProcessor,
- this,
- AuthenticationRepository,
- DeviceManager,
- MediaSourceManager);
- serviceCollection.AddSingleton(SessionManager);
+ serviceCollection.AddSingleton<ISessionManager, SessionManager>();
- serviceCollection.AddSingleton<IDlnaManager>(
- new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LoggerFactory, JsonSerializer, this));
+ serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
- CollectionManager = new CollectionManager(LibraryManager, ApplicationPaths, LocalizationManager, FileSystemManager, LibraryMonitor, LoggerFactory, ProviderManager);
- serviceCollection.AddSingleton(CollectionManager);
+ serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
- serviceCollection.AddSingleton(typeof(IPlaylistManager), typeof(PlaylistManager));
+ serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
- LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, LoggerFactory, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, FileSystemManager, () => ChannelManager);
- serviceCollection.AddSingleton(LiveTvManager);
+ serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
- UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager);
- serviceCollection.AddSingleton(UserViewManager);
+ serviceCollection.AddSingleton<LiveTvDtoService>();
+ serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
- NotificationManager = new NotificationManager(
- LoggerFactory.CreateLogger<NotificationManager>(),
- UserManager,
- ServerConfigurationManager);
- serviceCollection.AddSingleton(NotificationManager);
+ serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
- serviceCollection.AddSingleton<IDeviceDiscovery>(new DeviceDiscovery(ServerConfigurationManager));
+ serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
- ChapterManager = new ChapterManager(ItemRepository);
- serviceCollection.AddSingleton(ChapterManager);
+ serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
- EncodingManager = new MediaEncoder.EncodingManager(
- LoggerFactory.CreateLogger<MediaEncoder.EncodingManager>(),
- FileSystemManager,
- MediaEncoder,
- ChapterManager,
- LibraryManager);
- serviceCollection.AddSingleton(EncodingManager);
+ serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
- var activityLogRepo = GetActivityLogRepository();
- serviceCollection.AddSingleton(activityLogRepo);
- serviceCollection.AddSingleton<IActivityManager>(new ActivityManager(LoggerFactory, activityLogRepo, UserManager));
+ serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
- var authContext = new AuthorizationContext(AuthenticationRepository, UserManager);
- serviceCollection.AddSingleton<IAuthorizationContext>(authContext);
- serviceCollection.AddSingleton<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager));
+ serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
+ serviceCollection.AddSingleton<ISessionContext, SessionContext>();
- AuthService = new AuthService(LoggerFactory.CreateLogger<AuthService>(), authContext, ServerConfigurationManager, SessionManager, NetworkManager);
- serviceCollection.AddSingleton(AuthService);
+ serviceCollection.AddSingleton<IAuthService, AuthService>();
+ serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
- SubtitleEncoder = new MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder(
- LibraryManager,
- LoggerFactory.CreateLogger<MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(),
- ApplicationPaths,
- FileSystemManager,
- MediaEncoder,
- HttpClient,
- MediaSourceManager);
- serviceCollection.AddSingleton(SubtitleEncoder);
+ serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
- serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager));
+ serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
serviceCollection.AddSingleton<EncodingHelper>();
- serviceCollection.AddSingleton(typeof(IAttachmentExtractor), typeof(MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor));
+ serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+ }
- serviceCollection.AddSingleton(typeof(IQuickConnect), typeof(QuickConnect.QuickConnectManager));
- _displayPreferencesRepository.Initialize();
+ /// <summary>
+ /// Create services registered with the service container that need to be initialized at application startup.
+ /// </summary>
+ /// <returns>A task representing the service initialization operation.</returns>
+ public async Task InitializeServices()
+ {
+ var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
+ await localizationManager.LoadAll().ConfigureAwait(false);
+
+ _mediaEncoder = Resolve<IMediaEncoder>();
+ _sessionManager = Resolve<ISessionManager>();
+ _httpServer = Resolve<IHttpServer>();
+ _httpClient = Resolve<IHttpClient>();
- var userDataRepo = new SqliteUserDataRepository(LoggerFactory.CreateLogger<SqliteUserDataRepository>(), ApplicationPaths);
+ ((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
+ ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
+ ((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
SetStaticProperties();
- ((UserManager)UserManager).Initialize();
+ var userManager = (UserManager)Resolve<IUserManager>();
+ userManager.Initialize();
- ((UserDataManager)UserDataManager).Repository = userDataRepo;
- ItemRepository.Initialize(userDataRepo, UserManager);
- ((LibraryManager)LibraryManager).ItemRepository = ItemRepository;
- }
+ var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
+ ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
- /// <summary>
- /// Create services registered with the service container that need to be initialized at application startup.
- /// </summary>
- public void InitializeServices()
- {
- HttpServer = Resolve<IHttpServer>();
+ FindParts();
}
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
@@ -948,74 +743,37 @@ namespace Emby.Server.Implementations
}
/// <summary>
- /// Gets the user repository.
- /// </summary>
- /// <returns><see cref="Task{SqliteUserRepository}" />.</returns>
- private SqliteUserRepository GetUserRepository()
- {
- var repo = new SqliteUserRepository(
- LoggerFactory.CreateLogger<SqliteUserRepository>(),
- ApplicationPaths);
-
- repo.Initialize();
-
- return repo;
- }
-
- private IAuthenticationRepository GetAuthenticationRepository()
- {
- var repo = new AuthenticationRepository(LoggerFactory, ServerConfigurationManager);
-
- repo.Initialize();
-
- return repo;
- }
-
- private IActivityRepository GetActivityLogRepository()
- {
- var repo = new ActivityRepository(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager);
-
- repo.Initialize();
-
- return repo;
- }
-
- /// <summary>
/// Dirty hacks.
/// </summary>
private void SetStaticProperties()
{
- ItemRepository.ImageProcessor = ImageProcessor;
-
// For now there's no real way to inject these properly
- BaseItem.Logger = LoggerFactory.CreateLogger("BaseItem");
+ BaseItem.Logger = Resolve<ILogger<BaseItem>>();
BaseItem.ConfigurationManager = ServerConfigurationManager;
- BaseItem.LibraryManager = LibraryManager;
- BaseItem.ProviderManager = ProviderManager;
- BaseItem.LocalizationManager = LocalizationManager;
- BaseItem.ItemRepository = ItemRepository;
- User.UserManager = UserManager;
- BaseItem.FileSystem = FileSystemManager;
- BaseItem.UserDataManager = UserDataManager;
- BaseItem.ChannelManager = ChannelManager;
- Video.LiveTvManager = LiveTvManager;
- Folder.UserViewManager = UserViewManager;
- UserView.TVSeriesManager = TVSeriesManager;
- UserView.CollectionManager = CollectionManager;
- BaseItem.MediaSourceManager = MediaSourceManager;
- CollectionFolder.XmlSerializer = XmlSerializer;
- CollectionFolder.JsonSerializer = JsonSerializer;
+ BaseItem.LibraryManager = Resolve<ILibraryManager>();
+ BaseItem.ProviderManager = Resolve<IProviderManager>();
+ BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
+ BaseItem.ItemRepository = Resolve<IItemRepository>();
+ User.UserManager = Resolve<IUserManager>();
+ BaseItem.FileSystem = _fileSystemManager;
+ BaseItem.UserDataManager = Resolve<IUserDataManager>();
+ BaseItem.ChannelManager = Resolve<IChannelManager>();
+ Video.LiveTvManager = Resolve<ILiveTvManager>();
+ Folder.UserViewManager = Resolve<IUserViewManager>();
+ UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
+ UserView.CollectionManager = Resolve<ICollectionManager>();
+ BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+ CollectionFolder.XmlSerializer = _xmlSerializer;
+ CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
CollectionFolder.ApplicationHost = this;
- AuthenticatedAttribute.AuthService = AuthService;
+ AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
}
/// <summary>
- /// Finds the parts.
+ /// Finds plugin components and register them with the appropriate services.
/// </summary>
- public void FindParts()
+ private void FindParts()
{
- InstallationManager = ServiceProvider.GetService<IInstallationManager>();
-
if (!ServerConfigurationManager.Configuration.IsPortAuthorized)
{
ServerConfigurationManager.Configuration.IsPortAuthorized = true;
@@ -1028,34 +786,34 @@ namespace Emby.Server.Implementations
.Where(i => i != null)
.ToArray();
- HttpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
+ _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes());
- LibraryManager.AddParts(
+ Resolve<ILibraryManager>().AddParts(
GetExports<IResolverIgnoreRule>(),
GetExports<IItemResolver>(),
GetExports<IIntroProvider>(),
GetExports<IBaseItemComparer>(),
GetExports<ILibraryPostScanTask>());
- ProviderManager.AddParts(
+ Resolve<IProviderManager>().AddParts(
GetExports<IImageProvider>(),
GetExports<IMetadataService>(),
GetExports<IMetadataProvider>(),
GetExports<IMetadataSaver>(),
GetExports<IExternalId>());
- LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
+ Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
- SubtitleManager.AddParts(GetExports<ISubtitleProvider>());
+ Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
- ChannelManager.AddParts(GetExports<IChannel>());
+ Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
- MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>());
+ Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
- NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
- UserManager.AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
+ Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
+ Resolve<IUserManager>().AddParts(GetExports<IAuthenticationProvider>(), GetExports<IPasswordResetProvider>());
- IsoManager.AddParts(GetExports<IIsoMounter>());
+ Resolve<IIsoManager>().AddParts(GetExports<IIsoMounter>());
}
private IPlugin LoadPlugin(IPlugin plugin)
@@ -1162,16 +920,6 @@ namespace Emby.Server.Implementations
});
}
- private CertificateInfo GetCertificateInfo(bool generateCertificate)
- {
- // Custom cert
- return new CertificateInfo
- {
- Path = ServerConfigurationManager.Configuration.CertificatePath,
- Password = ServerConfigurationManager.Configuration.CertificatePassword
- };
- }
-
/// <summary>
/// Called when [configuration updated].
/// </summary>
@@ -1198,14 +946,13 @@ namespace Emby.Server.Implementations
}
}
- if (!HttpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
+ if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
{
requiresRestart = true;
}
var currentCertPath = CertificateInfo?.Path;
- var newCertInfo = GetCertificateInfo(false);
- var newCertPath = newCertInfo?.Path;
+ var newCertPath = ServerConfigurationManager.Configuration.CertificatePath;
if (!string.Equals(currentCertPath, newCertPath, StringComparison.OrdinalIgnoreCase))
{
@@ -1258,7 +1005,7 @@ namespace Emby.Server.Implementations
{
try
{
- await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1362,7 +1109,7 @@ namespace Emby.Server.Implementations
IsShuttingDown = IsShuttingDown,
Version = ApplicationVersionString,
WebSocketPortNumber = HttpPort,
- CompletedInstallations = InstallationManager.CompletedInstallations.ToArray(),
+ CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
Id = SystemId,
ProgramDataPath = ApplicationPaths.ProgramDataPath,
WebPath = ApplicationPaths.WebPath,
@@ -1370,9 +1117,6 @@ namespace Emby.Server.Implementations
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
- HttpServerPortNumber = HttpPort,
- SupportsHttps = SupportsHttps,
- HttpsPortNumber = HttpsPort,
OperatingSystem = OperatingSystem.Id.ToString(),
OperatingSystemDisplayName = OperatingSystem.Name,
CanSelfRestart = CanSelfRestart,
@@ -1382,15 +1126,14 @@ namespace Emby.Server.Implementations
ServerName = FriendlyName,
LocalAddress = localAddress,
SupportsLibraryMonitor = true,
- EncoderLocation = MediaEncoder.EncoderLocation,
+ EncoderLocation = _mediaEncoder.EncoderLocation,
SystemArchitecture = RuntimeInformation.OSArchitecture,
- SystemUpdateLevel = SystemUpdateLevel,
- PackageName = StartupOptions.PackageName
+ PackageName = _startupOptions.PackageName
};
}
public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
- => NetworkManager.GetMacAddresses()
+ => _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i))
.ToList();
@@ -1409,23 +1152,22 @@ namespace Emby.Server.Implementations
};
}
- public bool EnableHttps => SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps;
-
- public bool SupportsHttps => Certificate != null || ServerConfigurationManager.Configuration.IsBehindProxy;
+ /// <inheritdoc/>
+ public bool ListenWithHttps => Certificate != null && ServerConfigurationManager.Configuration.EnableHttps;
+ /// <inheritdoc/>
public async Task<string> GetLocalApiUrl(CancellationToken cancellationToken)
{
try
{
// Return the first matched address, if found, or the first known local address
var addresses = await GetLocalIpAddressesInternal(false, 1, cancellationToken).ConfigureAwait(false);
-
- foreach (var address in addresses)
+ if (addresses.Count == 0)
{
- return GetLocalApiUrl(address);
+ return null;
}
- return null;
+ return GetLocalApiUrl(addresses.First());
}
catch (Exception ex)
{
@@ -1468,22 +1210,24 @@ namespace Emby.Server.Implementations
return GetLocalApiUrl(ipAddress.ToString());
}
- /// <inheritdoc />
- public string GetLocalApiUrl(ReadOnlySpan<char> host)
+ /// <inheritdoc/>
+ public string GetLoopbackHttpApiUrl()
{
- var url = new StringBuilder(64);
- url.Append(EnableHttps ? "https://" : "http://")
- .Append(host)
- .Append(':')
- .Append(EnableHttps ? HttpsPort : HttpPort);
-
- string baseUrl = ServerConfigurationManager.Configuration.BaseUrl;
- if (baseUrl.Length != 0)
- {
- url.Append(baseUrl);
- }
+ return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+ }
- return url.ToString();
+ /// <inheritdoc/>
+ public string GetLocalApiUrl(ReadOnlySpan<char> host, string scheme = null, int? port = null)
+ {
+ // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
+ // not. For consistency, always trim the trailing slash.
+ return new UriBuilder
+ {
+ Scheme = scheme ?? (ListenWithHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp),
+ Host = host.ToString(),
+ Port = port ?? (ListenWithHttps ? HttpsPort : HttpPort),
+ Path = ServerConfigurationManager.Configuration.BaseUrl
+ }.ToString().TrimEnd('/');
}
public Task<List<IPAddress>> GetLocalIpAddresses(CancellationToken cancellationToken)
@@ -1502,7 +1246,7 @@ namespace Emby.Server.Implementations
if (addresses.Count == 0)
{
- addresses.AddRange(NetworkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
+ addresses.AddRange(_networkManager.GetLocalIpAddresses(ServerConfigurationManager.Configuration.IgnoreVirtualInterfaces));
}
var resultList = new List<IPAddress>();
@@ -1517,7 +1261,7 @@ namespace Emby.Server.Implementations
}
}
- var valid = await IsIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
+ var valid = await IsLocalIpAddressValidAsync(address, cancellationToken).ConfigureAwait(false);
if (valid)
{
resultList.Add(address);
@@ -1551,7 +1295,7 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
- private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
+ private async Task<bool> IsLocalIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken)
{
if (address.Equals(IPAddress.Loopback)
|| address.Equals(IPAddress.IPv6Loopback))
@@ -1559,8 +1303,7 @@ namespace Emby.Server.Implementations
return true;
}
- var apiUrl = GetLocalApiUrl(address);
- apiUrl += "/system/ping";
+ var apiUrl = GetLocalApiUrl(address) + "/system/ping";
if (_validAddressResults.TryGetValue(apiUrl, out var cachedResult))
{
@@ -1569,7 +1312,7 @@ namespace Emby.Server.Implementations
try
{
- using (var response = await HttpClient.SendAsync(
+ using (var response = await _httpClient.SendAsync(
new HttpRequestOptions
{
Url = apiUrl,
@@ -1622,7 +1365,7 @@ namespace Emby.Server.Implementations
try
{
- await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
+ await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -1743,14 +1486,8 @@ namespace Emby.Server.Implementations
Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
}
}
-
- _userRepository?.Dispose();
- _displayPreferencesRepository?.Dispose();
}
- _userRepository = null;
- _displayPreferencesRepository = null;
-
_disposed = true;
}
}
diff --git a/Emby.Server.Implementations/Archiving/ZipClient.cs b/Emby.Server.Implementations/Archiving/ZipClient.cs
index 4a6e5cfd7..591ae547d 100644
--- a/Emby.Server.Implementations/Archiving/ZipClient.cs
+++ b/Emby.Server.Implementations/Archiving/ZipClient.cs
@@ -22,10 +22,8 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAll(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
- using (var fileStream = File.OpenRead(sourceFile))
- {
- ExtractAll(fileStream, targetPath, overwriteExistingFiles);
- }
+ using var fileStream = File.OpenRead(sourceFile);
+ ExtractAll(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
@@ -36,67 +34,61 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles)
{
- using (var reader = ReaderFactory.Open(source))
+ using var reader = ReaderFactory.Open(source);
+ var options = new ExtractionOptions
{
- var options = new ExtractionOptions();
- options.ExtractFullPath = true;
-
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
+ ExtractFullPath = true
+ };
- reader.WriteAllToDirectory(targetPath, options);
+ if (overwriteExistingFiles)
+ {
+ options.Overwrite = true;
}
+
+ reader.WriteAllToDirectory(targetPath, options);
}
+ /// <inheritdoc />
public void ExtractAllFromZip(Stream source, string targetPath, bool overwriteExistingFiles)
{
- using (var reader = ZipReader.Open(source))
+ using var reader = ZipReader.Open(source);
+ var options = new ExtractionOptions
{
- var options = new ExtractionOptions();
- options.ExtractFullPath = true;
+ ExtractFullPath = true,
+ Overwrite = overwriteExistingFiles
+ };
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
-
- reader.WriteAllToDirectory(targetPath, options);
- }
+ reader.WriteAllToDirectory(targetPath, options);
}
+ /// <inheritdoc />
public void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles)
{
- using (var reader = GZipReader.Open(source))
+ using var reader = GZipReader.Open(source);
+ var options = new ExtractionOptions
{
- var options = new ExtractionOptions();
- options.ExtractFullPath = true;
+ ExtractFullPath = true,
+ Overwrite = overwriteExistingFiles
+ };
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
-
- reader.WriteAllToDirectory(targetPath, options);
- }
+ reader.WriteAllToDirectory(targetPath, options);
}
+ /// <inheritdoc />
public void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName)
{
- using (var reader = GZipReader.Open(source))
+ using var reader = GZipReader.Open(source);
+ if (reader.MoveToNextEntry())
{
- if (reader.MoveToNextEntry())
+ var entry = reader.Entry;
+
+ var filename = entry.Key;
+ if (string.IsNullOrWhiteSpace(filename))
{
- var entry = reader.Entry;
-
- var filename = entry.Key;
- if (string.IsNullOrWhiteSpace(filename))
- {
- filename = defaultFileName;
- }
- reader.WriteEntryToFile(Path.Combine(targetPath, filename));
+ filename = defaultFileName;
}
+
+ reader.WriteEntryToFile(Path.Combine(targetPath, filename));
}
}
@@ -108,10 +100,8 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFrom7z(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
- using (var fileStream = File.OpenRead(sourceFile))
- {
- ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
- }
+ using var fileStream = File.OpenRead(sourceFile);
+ ExtractAllFrom7z(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
@@ -122,21 +112,15 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFrom7z(Stream source, string targetPath, bool overwriteExistingFiles)
{
- using (var archive = SevenZipArchive.Open(source))
+ using var archive = SevenZipArchive.Open(source);
+ using var reader = archive.ExtractAllEntries();
+ var options = new ExtractionOptions
{
- using (var reader = archive.ExtractAllEntries())
- {
- var options = new ExtractionOptions();
- options.ExtractFullPath = true;
-
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
+ ExtractFullPath = true,
+ Overwrite = overwriteExistingFiles
+ };
- reader.WriteAllToDirectory(targetPath, options);
- }
- }
+ reader.WriteAllToDirectory(targetPath, options);
}
/// <summary>
@@ -147,10 +131,8 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFromTar(string sourceFile, string targetPath, bool overwriteExistingFiles)
{
- using (var fileStream = File.OpenRead(sourceFile))
- {
- ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
- }
+ using var fileStream = File.OpenRead(sourceFile);
+ ExtractAllFromTar(fileStream, targetPath, overwriteExistingFiles);
}
/// <summary>
@@ -161,21 +143,15 @@ namespace Emby.Server.Implementations.Archiving
/// <param name="overwriteExistingFiles">if set to <c>true</c> [overwrite existing files].</param>
public void ExtractAllFromTar(Stream source, string targetPath, bool overwriteExistingFiles)
{
- using (var archive = TarArchive.Open(source))
+ using var archive = TarArchive.Open(source);
+ using var reader = archive.ExtractAllEntries();
+ var options = new ExtractionOptions
{
- using (var reader = archive.ExtractAllEntries())
- {
- var options = new ExtractionOptions();
- options.ExtractFullPath = true;
+ ExtractFullPath = true,
+ Overwrite = overwriteExistingFiles
+ };
- if (overwriteExistingFiles)
- {
- options.Overwrite = true;
- }
-
- reader.WriteAllToDirectory(targetPath, options);
- }
- }
+ reader.WriteAllToDirectory(targetPath, options);
}
}
}
diff --git a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
index 93000ae12..7ae26bd8b 100644
--- a/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
+++ b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs
@@ -1,13 +1,15 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Branding;
namespace Emby.Server.Implementations.Branding
{
+ /// <summary>
+ /// A configuration factory for <see cref="BrandingOptions"/>.
+ /// </summary>
public class BrandingConfigurationFactory : IConfigurationFactory
{
+ /// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
index 96096e142..7f7c6a0be 100644
--- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs
+++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs
@@ -31,18 +31,18 @@ namespace Emby.Server.Implementations.Browser
/// 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="url">The URL.</param>
- private static void TryOpenUrl(IServerApplicationHost appHost, string url)
+ /// <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 + url);
+ appHost.LaunchUrl(baseUrl + relativeUrl);
}
catch (Exception ex)
{
var logger = appHost.Resolve<ILogger>();
- logger?.LogError(ex, "Failed to open browser window with URL {URL}", url);
+ logger?.LogError(ex, "Failed to open browser window with URL {URL}", relativeUrl);
}
}
}
diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
index 6016fed07..3e149cc82 100644
--- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
+++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs
@@ -1,7 +1,6 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Channels;
@@ -11,6 +10,9 @@ using MediaBrowser.Model.Dto;
namespace Emby.Server.Implementations.Channels
{
+ /// <summary>
+ /// A media source provider for channels.
+ /// </summary>
public class ChannelDynamicMediaSourceProvider : IMediaSourceProvider
{
private readonly ChannelManager _channelManager;
@@ -27,12 +29,9 @@ namespace Emby.Server.Implementations.Channels
/// <inheritdoc />
public Task<IEnumerable<MediaSourceInfo>> GetMediaSources(BaseItem item, CancellationToken cancellationToken)
{
- if (item.SourceType == SourceType.Channel)
- {
- return _channelManager.GetDynamicMediaSources(item, cancellationToken);
- }
-
- return Task.FromResult<IEnumerable<MediaSourceInfo>>(new List<MediaSourceInfo>());
+ return item.SourceType == SourceType.Channel
+ ? _channelManager.GetDynamicMediaSources(item, cancellationToken)
+ : Task.FromResult(Enumerable.Empty<MediaSourceInfo>());
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
index 62aeb9bcb..25cbfcf14 100644
--- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
+++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -11,20 +9,32 @@ using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Channels
{
+ /// <summary>
+ /// An image provider for channels.
+ /// </summary>
public class ChannelImageProvider : IDynamicImageProvider, IHasItemChangeMonitor
{
private readonly IChannelManager _channelManager;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelImageProvider"/> class.
+ /// </summary>
+ /// <param name="channelManager">The channel manager.</param>
public ChannelImageProvider(IChannelManager channelManager)
{
_channelManager = channelManager;
}
+ /// <inheritdoc />
+ public string Name => "Channel Image Provider";
+
+ /// <inheritdoc />
public IEnumerable<ImageType> GetSupportedImages(BaseItem item)
{
return GetChannel(item).GetSupportedChannelImages();
}
+ /// <inheritdoc />
public Task<DynamicImageResponse> GetImage(BaseItem item, ImageType type, CancellationToken cancellationToken)
{
var channel = GetChannel(item);
@@ -32,8 +42,7 @@ namespace Emby.Server.Implementations.Channels
return channel.GetChannelImage(type, cancellationToken);
}
- public string Name => "Channel Image Provider";
-
+ /// <inheritdoc />
public bool Supports(BaseItem item)
{
return item is Channel;
@@ -46,6 +55,7 @@ namespace Emby.Server.Implementations.Channels
return ((ChannelManager)_channelManager).GetChannelProvider(channel);
}
+ /// <inheritdoc />
public bool HasChanged(BaseItem item, IDirectoryService directoryService)
{
return GetSupportedImages(item).Any(i => !item.HasImage(i));
diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs
index 6e1baddfe..138832fb8 100644
--- a/Emby.Server.Implementations/Channels/ChannelManager.cs
+++ b/Emby.Server.Implementations/Channels/ChannelManager.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -29,10 +27,11 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Channels
{
+ /// <summary>
+ /// The LiveTV channel manager.
+ /// </summary>
public class ChannelManager : IChannelManager
{
- internal IChannel[] Channels { get; private set; }
-
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataManager;
private readonly IDtoService _dtoService;
@@ -43,11 +42,28 @@ namespace Emby.Server.Implementations.Channels
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
+ private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
+ new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
+
+ private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelManager"/> class.
+ /// </summary>
+ /// <param name="userManager">The user manager.</param>
+ /// <param name="dtoService">The dto service.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="userDataManager">The user data manager.</param>
+ /// <param name="jsonSerializer">The JSON serializer.</param>
+ /// <param name="providerManager">The provider manager.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
ILibraryManager libraryManager,
- ILoggerFactory loggerFactory,
+ ILogger<ChannelManager> logger,
IServerConfigurationManager config,
IFileSystem fileSystem,
IUserDataManager userDataManager,
@@ -57,7 +73,7 @@ namespace Emby.Server.Implementations.Channels
_userManager = userManager;
_dtoService = dtoService;
_libraryManager = libraryManager;
- _logger = loggerFactory.CreateLogger(nameof(ChannelManager));
+ _logger = logger;
_config = config;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
@@ -65,13 +81,17 @@ namespace Emby.Server.Implementations.Channels
_providerManager = providerManager;
}
+ internal IChannel[] Channels { get; private set; }
+
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
+ /// <inheritdoc />
public void AddParts(IEnumerable<IChannel> channels)
{
Channels = channels.ToArray();
}
+ /// <inheritdoc />
public bool EnableMediaSourceDisplay(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
@@ -80,15 +100,16 @@ namespace Emby.Server.Implementations.Channels
return !(channel is IDisableMediaSourceDisplay);
}
+ /// <inheritdoc />
public bool CanDelete(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id));
- var supportsDelete = channel as ISupportsDelete;
- return supportsDelete != null && supportsDelete.CanDelete(item);
+ return channel is ISupportsDelete supportsDelete && supportsDelete.CanDelete(item);
}
+ /// <inheritdoc />
public bool EnableMediaProbe(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
@@ -97,6 +118,7 @@ namespace Emby.Server.Implementations.Channels
return channel is ISupportsMediaProbe;
}
+ /// <inheritdoc />
public Task DeleteItem(BaseItem item)
{
var internalChannel = _libraryManager.GetItemById(item.ChannelId);
@@ -123,11 +145,16 @@ namespace Emby.Server.Implementations.Channels
.OrderBy(i => i.Name);
}
+ /// <summary>
+ /// Get the installed channel IDs.
+ /// </summary>
+ /// <returns>An <see cref="IEnumerable{T}"/> containing installed channel IDs.</returns>
public IEnumerable<Guid> GetInstalledChannelIds()
{
return GetAllChannels().Select(i => GetInternalChannelId(i.Name));
}
+ /// <inheritdoc />
public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
{
var user = query.UserId.Equals(Guid.Empty)
@@ -146,15 +173,13 @@ namespace Emby.Server.Implementations.Channels
{
try
{
- var hasAttributes = GetChannelProvider(i) as IHasFolderAttributes;
-
- return (hasAttributes != null && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
+ return (GetChannelProvider(i) is IHasFolderAttributes hasAttributes
+ && hasAttributes.Attributes.Contains("Recordings", StringComparer.OrdinalIgnoreCase)) == val;
}
catch
{
return false;
}
-
}).ToList();
}
@@ -171,7 +196,6 @@ namespace Emby.Server.Implementations.Channels
{
return false;
}
-
}).ToList();
}
@@ -188,9 +212,9 @@ namespace Emby.Server.Implementations.Channels
{
return false;
}
-
}).ToList();
}
+
if (query.IsFavorite.HasValue)
{
var val = query.IsFavorite.Value;
@@ -215,7 +239,6 @@ namespace Emby.Server.Implementations.Channels
{
return false;
}
-
}).ToList();
}
@@ -226,6 +249,7 @@ namespace Emby.Server.Implementations.Channels
{
all = all.Skip(query.StartIndex.Value).ToList();
}
+
if (query.Limit.HasValue)
{
all = all.Take(query.Limit.Value).ToList();
@@ -248,6 +272,7 @@ namespace Emby.Server.Implementations.Channels
};
}
+ /// <inheritdoc />
public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
{
var user = query.UserId.Equals(Guid.Empty)
@@ -256,11 +281,9 @@ namespace Emby.Server.Implementations.Channels
var internalResult = GetChannelsInternal(query);
- var dtoOptions = new DtoOptions()
- {
- };
+ var dtoOptions = new DtoOptions();
- //TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
+ // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues.
var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user);
var result = new QueryResult<BaseItemDto>
@@ -272,6 +295,12 @@ namespace Emby.Server.Implementations.Channels
return result;
}
+ /// <summary>
+ /// Refreshes the associated channels.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
+ /// <returns>The completed task.</returns>
public async Task RefreshChannels(IProgress<double> progress, CancellationToken cancellationToken)
{
var allChannelsList = GetAllChannels().ToList();
@@ -305,14 +334,7 @@ namespace Emby.Server.Implementations.Channels
private Channel GetChannelEntity(IChannel channel)
{
- var item = GetChannel(GetInternalChannelId(channel.Name));
-
- if (item == null)
- {
- item = GetChannel(channel, CancellationToken.None).Result;
- }
-
- return item;
+ return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result;
}
private List<MediaSourceInfo> GetSavedMediaSources(BaseItem item)
@@ -341,8 +363,8 @@ namespace Emby.Server.Implementations.Channels
}
catch
{
-
}
+
return;
}
@@ -351,6 +373,7 @@ namespace Emby.Server.Implementations.Channels
_jsonSerializer.SerializeToFile(mediaSources, path);
}
+ /// <inheritdoc />
public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken)
{
IEnumerable<MediaSourceInfo> results = GetSavedMediaSources(item);
@@ -360,16 +383,20 @@ namespace Emby.Server.Implementations.Channels
.ToList();
}
+ /// <summary>
+ /// Gets the dynamic media sources based on the provided item.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
+ /// <returns>The task representing the operation to get the media sources.</returns>
public async Task<IEnumerable<MediaSourceInfo>> GetDynamicMediaSources(BaseItem item, CancellationToken cancellationToken)
{
var channel = GetChannel(item.ChannelId);
var channelPlugin = GetChannelProvider(channel);
- var requiresCallback = channelPlugin as IRequiresMediaInfoCallback;
-
IEnumerable<MediaSourceInfo> results;
- if (requiresCallback != null)
+ if (channelPlugin is IRequiresMediaInfoCallback requiresCallback)
{
results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken)
.ConfigureAwait(false);
@@ -384,9 +411,6 @@ namespace Emby.Server.Implementations.Channels
.ToList();
}
- private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
- new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
-
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
{
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
@@ -409,7 +433,7 @@ namespace Emby.Server.Implementations.Channels
private static MediaSourceInfo NormalizeMediaSource(BaseItem item, MediaSourceInfo info)
{
- info.RunTimeTicks = info.RunTimeTicks ?? item.RunTimeTicks;
+ info.RunTimeTicks ??= item.RunTimeTicks;
return info;
}
@@ -444,18 +468,21 @@ namespace Emby.Server.Implementations.Channels
{
isNew = true;
}
+
item.Path = path;
if (!item.ChannelId.Equals(id))
{
forceUpdate = true;
}
+
item.ChannelId = id;
if (item.ParentId != parentFolderId)
{
forceUpdate = true;
}
+
item.ParentId = parentFolderId;
item.OfficialRating = GetOfficialRating(channelInfo.ParentalRating);
@@ -472,51 +499,56 @@ namespace Emby.Server.Implementations.Channels
_libraryManager.CreateItem(item, null);
}
- await item.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = !isNew && forceUpdate
- }, cancellationToken).ConfigureAwait(false);
+ await item.RefreshMetadata(
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = !isNew && forceUpdate
+ },
+ cancellationToken).ConfigureAwait(false);
return item;
}
private static string GetOfficialRating(ChannelParentalRating rating)
{
- switch (rating)
- {
- case ChannelParentalRating.Adult:
- return "XXX";
- case ChannelParentalRating.UsR:
- return "R";
- case ChannelParentalRating.UsPG13:
- return "PG-13";
- case ChannelParentalRating.UsPG:
- return "PG";
- default:
- return null;
- }
+ return rating switch
+ {
+ ChannelParentalRating.Adult => "XXX",
+ ChannelParentalRating.UsR => "R",
+ ChannelParentalRating.UsPG13 => "PG-13",
+ ChannelParentalRating.UsPG => "PG",
+ _ => null
+ };
}
+ /// <summary>
+ /// Gets a channel with the provided Guid.
+ /// </summary>
+ /// <param name="id">The Guid.</param>
+ /// <returns>The corresponding channel.</returns>
public Channel GetChannel(Guid id)
{
return _libraryManager.GetItemById(id) as Channel;
}
+ /// <inheritdoc />
public Channel GetChannel(string id)
{
return _libraryManager.GetItemById(id) as Channel;
}
+ /// <inheritdoc />
public ChannelFeatures[] GetAllChannelFeatures()
{
- return _libraryManager.GetItemIds(new InternalItemsQuery
- {
- IncludeItemTypes = new[] { typeof(Channel).Name },
- OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
-
- }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
+ return _libraryManager.GetItemIds(
+ new InternalItemsQuery
+ {
+ IncludeItemTypes = new[] { typeof(Channel).Name },
+ OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }
+ }).Select(i => GetChannelFeatures(i.ToString("N", CultureInfo.InvariantCulture))).ToArray();
}
+ /// <inheritdoc />
public ChannelFeatures GetChannelFeatures(string id)
{
if (string.IsNullOrEmpty(id))
@@ -530,15 +562,27 @@ namespace Emby.Server.Implementations.Channels
return GetChannelFeaturesDto(channel, channelProvider, channelProvider.GetChannelFeatures());
}
+ /// <summary>
+ /// Checks whether the provided Guid supports external transfer.
+ /// </summary>
+ /// <param name="channelId">The Guid.</param>
+ /// <returns>Whether or not the provided Guid supports external transfer.</returns>
public bool SupportsExternalTransfer(Guid channelId)
{
- //var channel = GetChannel(channelId);
var channelProvider = GetChannelProvider(channelId);
return channelProvider.GetChannelFeatures().SupportsContentDownloading;
}
- public ChannelFeatures GetChannelFeaturesDto(Channel channel,
+ /// <summary>
+ /// Gets the provided channel's supported features.
+ /// </summary>
+ /// <param name="channel">The channel.</param>
+ /// <param name="provider">The provider.</param>
+ /// <param name="features">The features.</param>
+ /// <returns>The supported features.</returns>
+ public ChannelFeatures GetChannelFeaturesDto(
+ Channel channel,
IChannel provider,
InternalChannelFeatures features)
{
@@ -567,9 +611,11 @@ namespace Emby.Server.Implementations.Channels
{
throw new ArgumentNullException(nameof(name));
}
+
return _libraryManager.GetNewItemId("Channel " + name, typeof(Channel));
}
+ /// <inheritdoc />
public async Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken)
{
var internalResult = await GetLatestChannelItemsInternal(query, cancellationToken).ConfigureAwait(false);
@@ -588,6 +634,7 @@ namespace Emby.Server.Implementations.Channels
return result;
}
+ /// <inheritdoc />
public async Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken)
{
var channels = GetAllChannels().Where(i => i is ISupportsLatestMedia).ToArray();
@@ -614,7 +661,7 @@ namespace Emby.Server.Implementations.Channels
query.IsFolder = false;
// hack for trailers, figure out a better way later
- var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.IndexOf("Trailer") != -1;
+ var sortByPremiereDate = channels.Length == 1 && channels[0].GetType().Name.Contains("Trailer", StringComparison.Ordinal);
if (sortByPremiereDate)
{
@@ -640,10 +687,12 @@ namespace Emby.Server.Implementations.Channels
{
var internalChannel = await GetChannel(channel, cancellationToken).ConfigureAwait(false);
- var query = new InternalItemsQuery();
- query.Parent = internalChannel;
- query.EnableTotalRecordCount = false;
- query.ChannelIds = new Guid[] { internalChannel.Id };
+ var query = new InternalItemsQuery
+ {
+ Parent = internalChannel,
+ EnableTotalRecordCount = false,
+ ChannelIds = new Guid[] { internalChannel.Id }
+ };
var result = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
@@ -651,17 +700,20 @@ namespace Emby.Server.Implementations.Channels
{
if (item is Folder folder)
{
- await GetChannelItemsInternal(new InternalItemsQuery
- {
- Parent = folder,
- EnableTotalRecordCount = false,
- ChannelIds = new Guid[] { internalChannel.Id }
-
- }, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
+ await GetChannelItemsInternal(
+ new InternalItemsQuery
+ {
+ Parent = folder,
+ EnableTotalRecordCount = false,
+ ChannelIds = new Guid[] { internalChannel.Id }
+ },
+ new SimpleProgress<double>(),
+ cancellationToken).ConfigureAwait(false);
}
}
}
+ /// <inheritdoc />
public async Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken)
{
// Get the internal channel entity
@@ -672,7 +724,8 @@ namespace Emby.Server.Implementations.Channels
var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId);
- var itemsResult = await GetChannelItems(channelProvider,
+ var itemsResult = await GetChannelItems(
+ channelProvider,
query.User,
parentItem is Channel ? null : parentItem.ExternalId,
null,
@@ -684,13 +737,12 @@ namespace Emby.Server.Implementations.Channels
{
query.Parent = channel;
}
+
query.ChannelIds = Array.Empty<Guid>();
// Not yet sure why this is causing a problem
query.GroupByPresentationUniqueKey = false;
- //_logger.LogDebug("GetChannelItemsInternal");
-
// null if came from cache
if (itemsResult != null)
{
@@ -707,12 +759,15 @@ namespace Emby.Server.Implementations.Channels
var deadItem = _libraryManager.GetItemById(deadId);
if (deadItem != null)
{
- _libraryManager.DeleteItem(deadItem, new DeleteOptions
- {
- DeleteFileLocation = false,
- DeleteFromExternalProvider = false
-
- }, parentItem, false);
+ _libraryManager.DeleteItem(
+ deadItem,
+ new DeleteOptions
+ {
+ DeleteFileLocation = false,
+ DeleteFromExternalProvider = false
+ },
+ parentItem,
+ false);
}
}
}
@@ -720,6 +775,7 @@ namespace Emby.Server.Implementations.Channels
return _libraryManager.GetItemsResult(query);
}
+ /// <inheritdoc />
public async Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken)
{
var internalResult = await GetChannelItemsInternal(query, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
@@ -735,7 +791,6 @@ namespace Emby.Server.Implementations.Channels
return result;
}
- private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
User user,
string externalFolderId,
@@ -743,7 +798,7 @@ namespace Emby.Server.Implementations.Channels
bool sortDescending,
CancellationToken cancellationToken)
{
- var userId = user == null ? null : user.Id.ToString("N", CultureInfo.InvariantCulture);
+ var userId = user?.Id.ToString("N", CultureInfo.InvariantCulture);
var cacheLength = CacheLength;
var cachePath = GetChannelDataCachePath(channel, userId, externalFolderId, sortField, sortDescending);
@@ -761,11 +816,9 @@ namespace Emby.Server.Implementations.Channels
}
catch (FileNotFoundException)
{
-
}
catch (IOException)
{
-
}
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -785,16 +838,14 @@ namespace Emby.Server.Implementations.Channels
}
catch (FileNotFoundException)
{
-
}
catch (IOException)
{
-
}
var query = new InternalChannelItemQuery
{
- UserId = user == null ? Guid.Empty : user.Id,
+ UserId = user?.Id ?? Guid.Empty,
SortBy = sortField,
SortDescending = sortDescending,
FolderId = externalFolderId
@@ -833,7 +884,8 @@ namespace Emby.Server.Implementations.Channels
}
}
- private string GetChannelDataCachePath(IChannel channel,
+ private string GetChannelDataCachePath(
+ IChannel channel,
string userId,
string externalFolderId,
ChannelItemSortField? sortField,
@@ -843,8 +895,7 @@ namespace Emby.Server.Implementations.Channels
var userCacheKey = string.Empty;
- var hasCacheKey = channel as IHasCacheKey;
- if (hasCacheKey != null)
+ if (channel is IHasCacheKey hasCacheKey)
{
userCacheKey = hasCacheKey.GetCacheKey(userId) ?? string.Empty;
}
@@ -858,6 +909,7 @@ namespace Emby.Server.Implementations.Channels
{
filename += "-sortField-" + sortField.Value;
}
+
if (sortDescending)
{
filename += "-sortDescending";
@@ -865,7 +917,8 @@ namespace Emby.Server.Implementations.Channels
filename = filename.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- return Path.Combine(_config.ApplicationPaths.CachePath,
+ return Path.Combine(
+ _config.ApplicationPaths.CachePath,
"channels",
channelId,
version,
@@ -919,60 +972,32 @@ namespace Emby.Server.Implementations.Channels
if (info.Type == ChannelItemType.Folder)
{
- if (info.FolderType == ChannelFolderType.MusicAlbum)
- {
- item = GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.FolderType == ChannelFolderType.MusicArtist)
- {
- item = GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.FolderType == ChannelFolderType.PhotoAlbum)
- {
- item = GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.FolderType == ChannelFolderType.Series)
- {
- item = GetItemById<Series>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.FolderType == ChannelFolderType.Season)
- {
- item = GetItemById<Season>(info.Id, channelProvider.Name, out isNew);
- }
- else
+ item = info.FolderType switch
{
- item = GetItemById<Folder>(info.Id, channelProvider.Name, out isNew);
- }
+ ChannelFolderType.MusicAlbum => GetItemById<MusicAlbum>(info.Id, channelProvider.Name, out isNew),
+ ChannelFolderType.MusicArtist => GetItemById<MusicArtist>(info.Id, channelProvider.Name, out isNew),
+ ChannelFolderType.PhotoAlbum => GetItemById<PhotoAlbum>(info.Id, channelProvider.Name, out isNew),
+ ChannelFolderType.Series => GetItemById<Series>(info.Id, channelProvider.Name, out isNew),
+ ChannelFolderType.Season => GetItemById<Season>(info.Id, channelProvider.Name, out isNew),
+ _ => GetItemById<Folder>(info.Id, channelProvider.Name, out isNew)
+ };
}
else if (info.MediaType == ChannelMediaType.Audio)
{
- if (info.ContentType == ChannelMediaContentType.Podcast)
- {
- item = GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew);
- }
- else
- {
- item = GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
- }
+ item = info.ContentType == ChannelMediaContentType.Podcast
+ ? GetItemById<AudioBook>(info.Id, channelProvider.Name, out isNew)
+ : GetItemById<Audio>(info.Id, channelProvider.Name, out isNew);
}
else
{
- if (info.ContentType == ChannelMediaContentType.Episode)
- {
- item = GetItemById<Episode>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.ContentType == ChannelMediaContentType.Movie)
- {
- item = GetItemById<Movie>(info.Id, channelProvider.Name, out isNew);
- }
- else if (info.ContentType == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer)
+ item = info.ContentType switch
{
- item = GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew);
- }
- else
- {
- item = GetItemById<Video>(info.Id, channelProvider.Name, out isNew);
- }
+ ChannelMediaContentType.Episode => GetItemById<Episode>(info.Id, channelProvider.Name, out isNew),
+ ChannelMediaContentType.Movie => GetItemById<Movie>(info.Id, channelProvider.Name, out isNew),
+ var x when x == ChannelMediaContentType.Trailer || info.ExtraType == ExtraType.Trailer
+ => GetItemById<Trailer>(info.Id, channelProvider.Name, out isNew),
+ _ => GetItemById<Video>(info.Id, channelProvider.Name, out isNew)
+ };
}
var enableMediaProbe = channelProvider is ISupportsMediaProbe;
@@ -981,7 +1006,6 @@ namespace Emby.Server.Implementations.Channels
{
item.RunTimeTicks = null;
}
-
else if (isNew || !enableMediaProbe)
{
item.RunTimeTicks = info.RunTimeTicks;
@@ -1014,26 +1038,24 @@ namespace Emby.Server.Implementations.Channels
}
}
- var hasArtists = item as IHasArtist;
- if (hasArtists != null)
+ if (item is IHasArtist hasArtists)
{
hasArtists.Artists = info.Artists.ToArray();
}
- var hasAlbumArtists = item as IHasAlbumArtist;
- if (hasAlbumArtists != null)
+ if (item is IHasAlbumArtist hasAlbumArtists)
{
hasAlbumArtists.AlbumArtists = info.AlbumArtists.ToArray();
}
- var trailer = item as Trailer;
- if (trailer != null)
+ if (item is Trailer trailer)
{
if (!info.TrailerTypes.SequenceEqual(trailer.TrailerTypes))
{
_logger.LogDebug("Forcing update due to TrailerTypes {0}", item.Name);
forceUpdate = true;
}
+
trailer.TrailerTypes = info.TrailerTypes.ToArray();
}
@@ -1057,6 +1079,7 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
_logger.LogDebug("Forcing update due to ChannelId {0}", item.Name);
}
+
item.ChannelId = internalChannelId;
if (!item.ParentId.Equals(parentFolderId))
@@ -1064,16 +1087,17 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
_logger.LogDebug("Forcing update due to parent folder Id {0}", item.Name);
}
+
item.ParentId = parentFolderId;
- var hasSeries = item as IHasSeries;
- if (hasSeries != null)
+ if (item is IHasSeries hasSeries)
{
if (!string.Equals(hasSeries.SeriesName, info.SeriesName, StringComparison.OrdinalIgnoreCase))
{
forceUpdate = true;
_logger.LogDebug("Forcing update due to SeriesName {0}", item.Name);
}
+
hasSeries.SeriesName = info.SeriesName;
}
@@ -1082,24 +1106,23 @@ namespace Emby.Server.Implementations.Channels
forceUpdate = true;
_logger.LogDebug("Forcing update due to ExternalId {0}", item.Name);
}
+
item.ExternalId = info.Id;
- var channelAudioItem = item as Audio;
- if (channelAudioItem != null)
+ if (item is Audio channelAudioItem)
{
channelAudioItem.ExtraType = info.ExtraType;
var mediaSource = info.MediaSources.FirstOrDefault();
- item.Path = mediaSource == null ? null : mediaSource.Path;
+ item.Path = mediaSource?.Path;
}
- var channelVideoItem = item as Video;
- if (channelVideoItem != null)
+ if (item is Video channelVideoItem)
{
channelVideoItem.ExtraType = info.ExtraType;
var mediaSource = info.MediaSources.FirstOrDefault();
- item.Path = mediaSource == null ? null : mediaSource.Path;
+ item.Path = mediaSource?.Path;
}
if (!string.IsNullOrEmpty(info.ImageUrl) && !item.HasImage(ImageType.Primary))
@@ -1156,7 +1179,7 @@ namespace Emby.Server.Implementations.Channels
}
}
- if (isNew || forceUpdate || item.DateLastRefreshed == default(DateTime))
+ if (isNew || forceUpdate || item.DateLastRefreshed == default)
{
_providerManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
}
diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
index 266d539d0..eeb49b8fe 100644
--- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
+++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Linq;
using System.Threading;
@@ -11,21 +9,34 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Channels
{
+ /// <summary>
+ /// A task to remove all non-installed channels from the database.
+ /// </summary>
public class ChannelPostScanTask
{
private readonly IChannelManager _channelManager;
- private readonly IUserManager _userManager;
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
- public ChannelPostScanTask(IChannelManager channelManager, IUserManager userManager, ILogger logger, ILibraryManager libraryManager)
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChannelPostScanTask"/> class.
+ /// </summary>
+ /// <param name="channelManager">The channel manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ public ChannelPostScanTask(IChannelManager channelManager, ILogger logger, ILibraryManager libraryManager)
{
_channelManager = channelManager;
- _userManager = userManager;
_logger = logger;
_libraryManager = libraryManager;
}
+ /// <summary>
+ /// Runs this task.
+ /// </summary>
+ /// <param name="progress">The progress.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>The completed task.</returns>
public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
{
CleanDatabase(cancellationToken);
diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
index 367efcb13..54b621e25 100644
--- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
+++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Threading;
@@ -7,29 +5,36 @@ using System.Threading.Tasks;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
-using MediaBrowser.Model.Globalization;
namespace Emby.Server.Implementations.Channels
{
+ /// <summary>
+ /// The "Refresh Channels" scheduled task.
+ /// </summary>
public class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask
{
private readonly IChannelManager _channelManager;
- private readonly IUserManager _userManager;
private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly ILocalizationManager _localization;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RefreshChannelsScheduledTask"/> class.
+ /// </summary>
+ /// <param name="channelManager">The channel manager.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="localization">The localization manager.</param>
public RefreshChannelsScheduledTask(
IChannelManager channelManager,
- IUserManager userManager,
ILogger<RefreshChannelsScheduledTask> logger,
ILibraryManager libraryManager,
ILocalizationManager localization)
{
_channelManager = channelManager;
- _userManager = userManager;
_logger = logger;
_libraryManager = libraryManager;
_localization = localization;
@@ -63,7 +68,7 @@ namespace Emby.Server.Implementations.Channels
await manager.RefreshChannels(new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false);
- await new ChannelPostScanTask(_channelManager, _userManager, _logger, _libraryManager).Run(progress, cancellationToken)
+ await new ChannelPostScanTask(_channelManager, _logger, _libraryManager).Run(progress, cancellationToken)
.ConfigureAwait(false);
}
@@ -72,7 +77,6 @@ namespace Emby.Server.Implementations.Channels
{
return new[]
{
-
// Every so often
new TaskTriggerInfo
{
diff --git a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
index 21ba0288e..c69a07e83 100644
--- a/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
+++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System.Collections.Generic;
using System.Linq;
using Emby.Server.Implementations.Images;
@@ -15,8 +13,18 @@ using MediaBrowser.Model.IO;
namespace Emby.Server.Implementations.Collections
{
+ /// <summary>
+ /// A collection image provider.
+ /// </summary>
public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet>
{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionImageProvider"/> class.
+ /// </summary>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="providerManager">The provider manager.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="imageProcessor">The image processor.</param>
public CollectionImageProvider(
IFileSystem fileSystem,
IProviderManager providerManager,
@@ -26,6 +34,7 @@ namespace Emby.Server.Implementations.Collections
{
}
+ /// <inheritdoc />
protected override bool Supports(BaseItem item)
{
// Right now this is the only way to prevent this image from getting created ahead of internet image providers
@@ -37,6 +46,7 @@ namespace Emby.Server.Implementations.Collections
return base.Supports(item);
}
+ /// <inheritdoc />
protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item)
{
var playlist = (BoxSet)item;
@@ -48,13 +58,10 @@ namespace Emby.Server.Implementations.Collections
var episode = subItem as Episode;
- if (episode != null)
+ var series = episode?.Series;
+ if (series != null && series.HasImage(ImageType.Primary))
{
- var series = episode.Series;
- if (series != null && series.HasImage(ImageType.Primary))
- {
- return series;
- }
+ return series;
}
if (subItem.HasImage(ImageType.Primary))
@@ -80,6 +87,7 @@ namespace Emby.Server.Implementations.Collections
.ToList();
}
+ /// <inheritdoc />
protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)
{
return CreateSingleImage(itemsWithImages, outputPathWithoutExtension, ImageType.Primary);
diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs
index 321952874..7c518d483 100644
--- a/Emby.Server.Implementations/Collections/CollectionManager.cs
+++ b/Emby.Server.Implementations/Collections/CollectionManager.cs
@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -23,6 +21,9 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Collections
{
+ /// <summary>
+ /// The collection manager.
+ /// </summary>
public class CollectionManager : ICollectionManager
{
private readonly ILibraryManager _libraryManager;
@@ -33,6 +34,16 @@ namespace Emby.Server.Implementations.Collections
private readonly ILocalizationManager _localizationManager;
private readonly IApplicationPaths _appPaths;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionManager"/> class.
+ /// </summary>
+ /// <param name="libraryManager">The library manager.</param>
+ /// <param name="appPaths">The application paths.</param>
+ /// <param name="localizationManager">The localization manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ /// <param name="iLibraryMonitor">The library monitor.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
+ /// <param name="providerManager">The provider manager.</param>
public CollectionManager(
ILibraryManager libraryManager,
IApplicationPaths appPaths,
@@ -51,8 +62,13 @@ namespace Emby.Server.Implementations.Collections
_appPaths = appPaths;
}
+ /// <inheritdoc />
public event EventHandler<CollectionCreatedEventArgs> CollectionCreated;
+
+ /// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsAddedToCollection;
+
+ /// <inheritdoc />
public event EventHandler<CollectionModifiedEventArgs> ItemsRemovedFromCollection;
private IEnumerable<Folder> FindFolders(string path)
@@ -109,11 +125,12 @@ namespace Emby.Server.Implementations.Collections
{
var folder = GetCollectionsFolder(false).Result;
- return folder == null ?
- new List<BoxSet>() :
- folder.GetChildren(user, true).OfType<BoxSet>();
+ return folder == null
+ ? Enumerable.Empty<BoxSet>()
+ : folder.GetChildren(user, true).OfType<BoxSet>();
}
+ /// <inheritdoc />
public BoxSet CreateCollection(CollectionCreationOptions options)
{
var name = options.Name;
@@ -178,11 +195,13 @@ namespace Emby.Server.Implementations.Collections
}
}
+ /// <inheritdoc />
public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
{
AddToCollection(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)));
@@ -191,7 +210,6 @@ namespace Emby.Server.Implementations.Collections
private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
-
if (collection == null)
{
throw new ArgumentException("No collection exists with the supplied Id");
@@ -246,11 +264,13 @@ 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)
{
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
@@ -289,10 +309,13 @@ namespace Emby.Server.Implementations.Collections
}
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
- _providerManager.QueueRefresh(collection.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- }, RefreshPriority.High);
+ _providerManager.QueueRefresh(
+ collection.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
ItemsRemovedFromCollection?.Invoke(this, new CollectionModifiedEventArgs
{
@@ -301,6 +324,7 @@ namespace Emby.Server.Implementations.Collections
});
}
+ /// <inheritdoc />
public IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user)
{
var results = new Dictionary<Guid, BaseItem>();
@@ -309,9 +333,7 @@ namespace Emby.Server.Implementations.Collections
foreach (var item in items)
{
- var grouping = item as ISupportsBoxSetGrouping;
-
- if (grouping == null)
+ if (!(item is ISupportsBoxSetGrouping))
{
results[item.Id] = item;
}
@@ -341,12 +363,21 @@ namespace Emby.Server.Implementations.Collections
}
}
+ /// <summary>
+ /// The collection manager entry point.
+ /// </summary>
public sealed class CollectionManagerEntryPoint : IServerEntryPoint
{
private readonly CollectionManager _collectionManager;
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CollectionManagerEntryPoint"/> class.
+ /// </summary>
+ /// <param name="collectionManager">The collection manager.</param>
+ /// <param name="config">The server configuration manager.</param>
+ /// <param name="logger">The logger.</param>
public CollectionManagerEntryPoint(
ICollectionManager collectionManager,
IServerConfigurationManager config,
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index f407317ec..305e67e8c 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -67,23 +67,22 @@ namespace Emby.Server.Implementations.Configuration
/// <summary>
/// Updates the metadata path.
/// </summary>
+ /// <exception cref="UnauthorizedAccessException">If the directory does not exist, and the caller does not have the required permission to create it.</exception>
+ /// <exception cref="NotSupportedException">If there is a custom path transcoding path specified, but it is invalid.</exception>
+ /// <exception cref="IOException">If the directory does not exist, and it also could not be created.</exception>
private void UpdateMetadataPath()
{
- if (string.IsNullOrWhiteSpace(Configuration.MetadataPath))
- {
- ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Path.Combine(ApplicationPaths.ProgramDataPath, "metadata");
- }
- else
- {
- ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = Configuration.MetadataPath;
- }
+ ((ServerApplicationPaths)ApplicationPaths).InternalMetadataPath = string.IsNullOrWhiteSpace(Configuration.MetadataPath)
+ ? ApplicationPaths.DefaultInternalMetadataPath
+ : Configuration.MetadataPath;
+ Directory.CreateDirectory(ApplicationPaths.InternalMetadataPath);
}
/// <summary>
/// Replaces the configuration.
/// </summary>
/// <param name="newConfiguration">The new configuration.</param>
- /// <exception cref="DirectoryNotFoundException"></exception>
+ /// <exception cref="DirectoryNotFoundException">If the configuration path doesn't exist.</exception>
public override void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration)
{
var newConfig = (ServerConfiguration)newConfiguration;
@@ -194,12 +193,6 @@ namespace Emby.Server.Implementations.Configuration
changed = true;
}
- if (!config.CameraUploadUpgraded)
- {
- config.CameraUploadUpgraded = true;
- changed = true;
- }
-
if (!config.CollectionsUpgraded)
{
config.CollectionsUpgraded = true;
diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index 20bdd18e7..dea9b6682 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -1,7 +1,6 @@
using System.Collections.Generic;
using Emby.Server.Implementations.HttpServer;
using Emby.Server.Implementations.Updates;
-using MediaBrowser.Providers.Music;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Emby.Server.Implementations
@@ -18,7 +17,7 @@ namespace Emby.Server.Implementations
{
{ HostWebClientKey, bool.TrueString },
{ HttpListenerHost.DefaultRedirectKey, "web/index.html" },
- { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest.json" },
+ { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.TrueString }
diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
index de83b023d..a037415a9 100644
--- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
+++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
@@ -31,7 +31,7 @@ namespace Emby.Server.Implementations.Cryptography
private RandomNumberGenerator _randomNumberGenerator;
- private bool _disposed = false;
+ private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="CryptographyProvider"/> class.
@@ -56,15 +56,13 @@ namespace Emby.Server.Implementations.Cryptography
{
// downgrading for now as we need this library to be dotnetstandard compliant
// with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
- if (method == DefaultHashMethod)
+ if (method != DefaultHashMethod)
{
- using (var r = new Rfc2898DeriveBytes(bytes, salt, iterations))
- {
- return r.GetBytes(32);
- }
+ throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
}
- throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
+ using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
+ return r.GetBytes(32);
}
/// <inheritdoc />
@@ -74,25 +72,22 @@ namespace Emby.Server.Implementations.Cryptography
{
return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
}
- else if (_supportedHashMethods.Contains(hashMethod))
+
+ if (!_supportedHashMethods.Contains(hashMethod))
+ {
+ throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ }
+
+ using var h = HashAlgorithm.Create(hashMethod);
+ if (salt.Length == 0)
{
- using (var h = HashAlgorithm.Create(hashMethod))
- {
- if (salt.Length == 0)
- {
- return h.ComputeHash(bytes);
- }
- else
- {
- byte[] salted = new byte[bytes.Length + salt.Length];
- Array.Copy(bytes, salted, bytes.Length);
- Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
- return h.ComputeHash(salted);
- }
- }
+ return h.ComputeHash(bytes);
}
- throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+ byte[] salted = new byte[bytes.Length + salt.Length];
+ Array.Copy(bytes, salted, bytes.Length);
+ Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
+ return h.ComputeHash(salted);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs
index e7c5270b9..716e5071d 100644
--- a/Emby.Server.Implementations/Data/SqliteExtensions.cs
+++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs
@@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
-using System.IO;
-using MediaBrowser.Model.Serialization;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
@@ -109,25 +107,6 @@ namespace Emby.Server.Implementations.Data
return null;
}
- /// <summary>
- /// Serializes to bytes.
- /// </summary>
- /// <returns>System.Byte[][].</returns>
- /// <exception cref="ArgumentNullException">obj</exception>
- public static byte[] SerializeToBytes(this IJsonSerializer json, object obj)
- {
- if (obj == null)
- {
- throw new ArgumentNullException(nameof(obj));
- }
-
- using (var stream = new MemoryStream())
- {
- json.SerializeToStream(obj, stream);
- return stream.ToArray();
- }
- }
-
public static void Attach(SQLiteDatabaseConnection db, string path, string alias)
{
var commandText = string.Format(
@@ -383,11 +362,11 @@ namespace Emby.Server.Implementations.Data
}
}
- public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement This)
+ public static IEnumerable<IReadOnlyList<IResultSetValue>> ExecuteQuery(this IStatement statement)
{
- while (This.MoveNext())
+ while (statement.MoveNext())
{
- yield return This.Current;
+ yield return statement.Current;
}
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
index 33ff74bb5..ca5cd6fdd 100644
--- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs
@@ -39,12 +39,11 @@ namespace Emby.Server.Implementations.Data
{
private const string ChaptersTableName = "Chapters2";
- /// <summary>
- /// The _app paths
- /// </summary>
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
private readonly ILocalizationManager _localization;
+ // TODO: Remove this dependency. GetImageCacheTag() is the only method used and it can be converted to a static helper method
+ private readonly IImageProcessor _imageProcessor;
private readonly TypeMapper _typeMapper;
private readonly JsonSerializerOptions _jsonOptions;
@@ -71,7 +70,8 @@ namespace Emby.Server.Implementations.Data
IServerConfigurationManager config,
IServerApplicationHost appHost,
ILogger<SqliteItemRepository> logger,
- ILocalizationManager localization)
+ ILocalizationManager localization,
+ IImageProcessor imageProcessor)
: base(logger)
{
if (config == null)
@@ -82,6 +82,7 @@ namespace Emby.Server.Implementations.Data
_config = config;
_appHost = appHost;
_localization = localization;
+ _imageProcessor = imageProcessor;
_typeMapper = new TypeMapper();
_jsonOptions = JsonDefaults.GetOptions();
@@ -98,8 +99,6 @@ namespace Emby.Server.Implementations.Data
/// <inheritdoc />
protected override TempStoreMode TempStore => TempStoreMode.Memory;
- public IImageProcessor ImageProcessor { get; set; }
-
/// <summary>
/// Opens the connection to the database
/// </summary>
@@ -1991,7 +1990,14 @@ namespace Emby.Server.Implementations.Data
if (!string.IsNullOrEmpty(chapter.ImagePath))
{
- chapter.ImageTag = ImageProcessor.GetImageCacheTag(item, chapter);
+ try
+ {
+ chapter.ImageTag = _imageProcessor.GetImageCacheTag(item, chapter);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Failed to create image cache tag.");
+ }
}
}
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 22955850a..6ee6230fc 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -375,5 +375,15 @@ namespace Emby.Server.Implementations.Data
return userData;
}
+
+ /// <inheritdoc/>
+ /// <remarks>
+ /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
+ /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
+ /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
+ /// </remarks>
+ protected override void Dispose(bool dispose)
+ {
+ }
}
}
diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs
index adb8e793d..2283f2433 100644
--- a/Emby.Server.Implementations/Devices/DeviceManager.cs
+++ b/Emby.Server.Implementations/Devices/DeviceManager.cs
@@ -5,27 +5,18 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Security;
-using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Devices;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Users;
-using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Devices
{
@@ -33,42 +24,27 @@ namespace Emby.Server.Implementations.Devices
{
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
- private readonly IFileSystem _fileSystem;
- private readonly ILibraryMonitor _libraryMonitor;
private readonly IServerConfigurationManager _config;
- private readonly ILibraryManager _libraryManager;
- private readonly ILocalizationManager _localizationManager;
-
private readonly IAuthenticationRepository _authRepo;
+ private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
- public event EventHandler<GenericEventArgs<CameraImageUploadInfo>> CameraImageUploaded;
- private readonly object _cameraUploadSyncLock = new object();
private readonly object _capabilitiesSyncLock = new object();
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
- ILibraryManager libraryManager,
- ILocalizationManager localizationManager,
IUserManager userManager,
- IFileSystem fileSystem,
- ILibraryMonitor libraryMonitor,
IServerConfigurationManager config)
{
_json = json;
_userManager = userManager;
- _fileSystem = fileSystem;
- _libraryMonitor = libraryMonitor;
_config = config;
- _libraryManager = libraryManager;
- _localizationManager = localizationManager;
_authRepo = authRepo;
+ _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
-
- private Dictionary<string, ClientCapabilities> _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
{
var path = Path.Combine(GetDevicePath(deviceId), "capabilities.json");
@@ -194,172 +170,6 @@ namespace Emby.Server.Implementations.Devices
return Path.Combine(GetDevicesPath(), id.GetMD5().ToString("N", CultureInfo.InvariantCulture));
}
- public ContentUploadHistory GetCameraUploadHistory(string deviceId)
- {
- var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
-
- lock (_cameraUploadSyncLock)
- {
- try
- {
- return _json.DeserializeFromFile<ContentUploadHistory>(path);
- }
- catch (IOException)
- {
- return new ContentUploadHistory
- {
- DeviceId = deviceId
- };
- }
- }
- }
-
- public async Task AcceptCameraUpload(string deviceId, Stream stream, LocalFileInfo file)
- {
- var device = GetDevice(deviceId, false);
- var uploadPathInfo = GetUploadPath(device);
-
- var path = uploadPathInfo.Item1;
-
- if (!string.IsNullOrWhiteSpace(file.Album))
- {
- path = Path.Combine(path, _fileSystem.GetValidFilename(file.Album));
- }
-
- path = Path.Combine(path, file.Name);
- path = Path.ChangeExtension(path, MimeTypes.ToExtension(file.MimeType) ?? "jpg");
-
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- await EnsureLibraryFolder(uploadPathInfo.Item2, uploadPathInfo.Item3).ConfigureAwait(false);
-
- _libraryMonitor.ReportFileSystemChangeBeginning(path);
-
- try
- {
- using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
- {
- await stream.CopyToAsync(fs).ConfigureAwait(false);
- }
-
- AddCameraUpload(deviceId, file);
- }
- finally
- {
- _libraryMonitor.ReportFileSystemChangeComplete(path, true);
- }
-
- if (CameraImageUploaded != null)
- {
- CameraImageUploaded?.Invoke(this, new GenericEventArgs<CameraImageUploadInfo>
- {
- Argument = new CameraImageUploadInfo
- {
- Device = device,
- FileInfo = file
- }
- });
- }
- }
-
- private void AddCameraUpload(string deviceId, LocalFileInfo file)
- {
- var path = Path.Combine(GetDevicePath(deviceId), "camerauploads.json");
- Directory.CreateDirectory(Path.GetDirectoryName(path));
-
- lock (_cameraUploadSyncLock)
- {
- ContentUploadHistory history;
-
- try
- {
- history = _json.DeserializeFromFile<ContentUploadHistory>(path);
- }
- catch (IOException)
- {
- history = new ContentUploadHistory
- {
- DeviceId = deviceId
- };
- }
-
- history.DeviceId = deviceId;
-
- var list = history.FilesUploaded.ToList();
- list.Add(file);
- history.FilesUploaded = list.ToArray();
-
- _json.SerializeToFile(history, path);
- }
- }
-
- internal Task EnsureLibraryFolder(string path, string name)
- {
- var existingFolders = _libraryManager
- .RootFolder
- .Children
- .OfType<Folder>()
- .Where(i => _fileSystem.AreEqual(path, i.Path) || _fileSystem.ContainsSubPath(i.Path, path))
- .ToList();
-
- if (existingFolders.Count > 0)
- {
- return Task.CompletedTask;
- }
-
- Directory.CreateDirectory(path);
-
- var libraryOptions = new LibraryOptions
- {
- PathInfos = new[] { new MediaPathInfo { Path = path } },
- EnablePhotos = true,
- EnableRealtimeMonitor = false,
- SaveLocalMetadata = true
- };
-
- if (string.IsNullOrWhiteSpace(name))
- {
- name = _localizationManager.GetLocalizedString("HeaderCameraUploads");
- }
-
- return _libraryManager.AddVirtualFolder(name, CollectionType.HomeVideos, libraryOptions, true);
- }
-
- private Tuple<string, string, string> GetUploadPath(DeviceInfo device)
- {
- var config = _config.GetUploadOptions();
- var path = config.CameraUploadPath;
-
- if (string.IsNullOrWhiteSpace(path))
- {
- path = DefaultCameraUploadsPath;
- }
-
- var topLibraryPath = path;
-
- if (config.EnableCameraUploadSubfolders)
- {
- path = Path.Combine(path, _fileSystem.GetValidFilename(device.Name));
- }
-
- return new Tuple<string, string, string>(path, topLibraryPath, null);
- }
-
- internal string GetUploadsPath()
- {
- var config = _config.GetUploadOptions();
- var path = config.CameraUploadPath;
-
- if (string.IsNullOrWhiteSpace(path))
- {
- path = DefaultCameraUploadsPath;
- }
-
- return path;
- }
-
- private string DefaultCameraUploadsPath => Path.Combine(_config.CommonApplicationPaths.DataPath, "camerauploads");
-
public bool CanAccessDevice(User user, string deviceId)
{
if (user == null)
@@ -399,102 +209,4 @@ namespace Emby.Server.Implementations.Devices
return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
}
}
-
- public class DeviceManagerEntryPoint : IServerEntryPoint
- {
- private readonly DeviceManager _deviceManager;
- private readonly IServerConfigurationManager _config;
- private ILogger _logger;
-
- public DeviceManagerEntryPoint(
- IDeviceManager deviceManager,
- IServerConfigurationManager config,
- ILogger<DeviceManagerEntryPoint> logger)
- {
- _deviceManager = (DeviceManager)deviceManager;
- _config = config;
- _logger = logger;
- }
-
- public async Task RunAsync()
- {
- if (!_config.Configuration.CameraUploadUpgraded && _config.Configuration.IsStartupWizardCompleted)
- {
- var path = _deviceManager.GetUploadsPath();
-
- if (Directory.Exists(path))
- {
- try
- {
- await _deviceManager.EnsureLibraryFolder(path, null).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating camera uploads library");
- }
-
- _config.Configuration.CameraUploadUpgraded = true;
- _config.SaveConfiguration();
- }
- }
- }
-
- #region IDisposable Support
- private bool disposedValue = false; // To detect redundant calls
-
- protected virtual void Dispose(bool disposing)
- {
- if (!disposedValue)
- {
- if (disposing)
- {
- // TODO: dispose managed state (managed objects).
- }
-
- // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
- // TODO: set large fields to null.
-
- disposedValue = true;
- }
- }
-
- // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
- // ~DeviceManagerEntryPoint() {
- // // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
- // Dispose(false);
- // }
-
- // This code added to correctly implement the disposable pattern.
- public void Dispose()
- {
- // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
- Dispose(true);
- // TODO: uncomment the following line if the finalizer is overridden above.
- // GC.SuppressFinalize(this);
- }
- #endregion
- }
-
- public class DevicesConfigStore : IConfigurationFactory
- {
- public IEnumerable<ConfigurationStore> GetConfigurations()
- {
- return new ConfigurationStore[]
- {
- new ConfigurationStore
- {
- Key = "devices",
- ConfigurationType = typeof(DevicesOptions)
- }
- };
- }
- }
-
- public static class UploadConfigExtension
- {
- public static DevicesOptions GetUploadOptions(this IConfigurationManager config)
- {
- return config.GetConfiguration<DevicesOptions>("devices");
- }
- }
}
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 34a342cf7..c4b65d265 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -38,21 +38,23 @@ namespace Emby.Server.Implementations.Dto
private readonly IProviderManager _providerManager;
private readonly IApplicationHost _appHost;
- private readonly Func<IMediaSourceManager> _mediaSourceManager;
- private readonly Func<ILiveTvManager> _livetvManager;
+ private readonly IMediaSourceManager _mediaSourceManager;
+ private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
+
+ private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
public DtoService(
- ILoggerFactory loggerFactory,
+ ILogger<DtoService> logger,
ILibraryManager libraryManager,
IUserDataManager userDataRepository,
IItemRepository itemRepo,
IImageProcessor imageProcessor,
IProviderManager providerManager,
IApplicationHost appHost,
- Func<IMediaSourceManager> mediaSourceManager,
- Func<ILiveTvManager> livetvManager)
+ IMediaSourceManager mediaSourceManager,
+ Lazy<ILiveTvManager> livetvManagerFactory)
{
- _logger = loggerFactory.CreateLogger(nameof(DtoService));
+ _logger = logger;
_libraryManager = libraryManager;
_userDataRepository = userDataRepository;
_itemRepo = itemRepo;
@@ -60,7 +62,7 @@ namespace Emby.Server.Implementations.Dto
_providerManager = providerManager;
_appHost = appHost;
_mediaSourceManager = mediaSourceManager;
- _livetvManager = livetvManager;
+ _livetvManagerFactory = livetvManagerFactory;
}
/// <summary>
@@ -125,12 +127,12 @@ namespace Emby.Server.Implementations.Dto
if (programTuples.Count > 0)
{
- _livetvManager().AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
+ LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
}
if (channelTuples.Count > 0)
{
- _livetvManager().AddChannelInfo(channelTuples, options, user);
+ LivetvManager.AddChannelInfo(channelTuples, options, user);
}
return returnItems;
@@ -142,12 +144,12 @@ namespace Emby.Server.Implementations.Dto
if (item is LiveTvChannel tvChannel)
{
var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
- _livetvManager().AddChannelInfo(list, options, user);
+ LivetvManager.AddChannelInfo(list, options, user);
}
else if (item is LiveTvProgram)
{
var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
- var task = _livetvManager().AddInfoToProgramDto(list, options.Fields, user);
+ var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
Task.WaitAll(task);
}
@@ -223,7 +225,7 @@ namespace Emby.Server.Implementations.Dto
if (item is IHasMediaSources
&& options.ContainsField(ItemFields.MediaSources))
{
- dto.MediaSources = _mediaSourceManager().GetStaticMediaSources(item, true, user).ToArray();
+ dto.MediaSources = _mediaSourceManager.GetStaticMediaSources(item, true, user).ToArray();
NormalizeMediaSourceContainers(dto);
}
@@ -254,7 +256,7 @@ namespace Emby.Server.Implementations.Dto
dto.Etag = item.GetEtag(user);
}
- var liveTvManager = _livetvManager();
+ var liveTvManager = LivetvManager;
var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
if (activeRecording != null)
{
@@ -1045,7 +1047,7 @@ namespace Emby.Server.Implementations.Dto
}
else
{
- mediaStreams = _mediaSourceManager().GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
+ mediaStreams = _mediaSourceManager.GetStaticMediaSources(item, true)[0].MediaStreams.ToArray();
}
dto.MediaStreams = mediaStreams;
diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
index 3d065f70a..b69a126b3 100644
--- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj
+++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj
@@ -1,4 +1,9 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+ <PropertyGroup>
+ <ProjectGuid>{E383961B-9356-4D5D-8233-9A1079D03055}</ProjectGuid>
+ </PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
@@ -29,14 +34,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.3" />
- <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.3" />
- <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.3" />
- <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.3" />
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.4" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.4" />
<PackageReference Include="Mono.Nat" Version="2.0.1" />
+ <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" />
<PackageReference Include="ServiceStack.Text.Core" Version="5.8.0" />
<PackageReference Include="sharpcompress" Version="0.25.0" />
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" />
+ <PackageReference Include="DotNet.Glob" Version="3.0.9" />
</ItemGroup>
<ItemGroup>
diff --git a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
index e290c62e1..878cee23c 100644
--- a/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
+++ b/Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
@@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Text;
@@ -26,10 +27,10 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IServerConfigurationManager _config;
private readonly IDeviceDiscovery _deviceDiscovery;
- private readonly object _createdRulesLock = new object();
- private List<IPEndPoint> _createdRules = new List<IPEndPoint>();
+ private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
+
private Timer _timer;
- private string _lastConfigIdentifier;
+ private string _configIdentifier;
private bool _disposed = false;
@@ -60,16 +61,20 @@ namespace Emby.Server.Implementations.EntryPoints
return new StringBuilder(32)
.Append(config.EnableUPnP).Append(Separator)
.Append(config.PublicPort).Append(Separator)
+ .Append(config.PublicHttpsPort).Append(Separator)
.Append(_appHost.HttpPort).Append(Separator)
.Append(_appHost.HttpsPort).Append(Separator)
- .Append(_appHost.EnableHttps).Append(Separator)
+ .Append(_appHost.ListenWithHttps).Append(Separator)
.Append(config.EnableRemoteAccess).Append(Separator)
.ToString();
}
private void OnConfigurationUpdated(object sender, EventArgs e)
{
- if (!string.Equals(_lastConfigIdentifier, GetConfigIdentifier(), StringComparison.OrdinalIgnoreCase))
+ var oldConfigIdentifier = _configIdentifier;
+ _configIdentifier = GetConfigIdentifier();
+
+ if (!string.Equals(_configIdentifier, oldConfigIdentifier, StringComparison.OrdinalIgnoreCase))
{
Stop();
Start();
@@ -93,21 +98,19 @@ namespace Emby.Server.Implementations.EntryPoints
return;
}
- _logger.LogDebug("Starting NAT discovery");
+ _logger.LogInformation("Starting NAT discovery");
NatUtility.DeviceFound += OnNatUtilityDeviceFound;
NatUtility.StartDiscovery();
- _timer = new Timer(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
+ _timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
-
- _lastConfigIdentifier = GetConfigIdentifier();
}
private void Stop()
{
- _logger.LogDebug("Stopping NAT discovery");
+ _logger.LogInformation("Stopping NAT discovery");
NatUtility.StopDiscovery();
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
@@ -117,26 +120,16 @@ namespace Emby.Server.Implementations.EntryPoints
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
}
- private void ClearCreatedRules(object state)
- {
- lock (_createdRulesLock)
- {
- _createdRules.Clear();
- }
- }
-
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
{
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
}
- private void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
+ private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
{
try
{
- var device = e.Device;
-
- CreateRules(device);
+ await CreateRules(e.Device).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -144,7 +137,7 @@ namespace Emby.Server.Implementations.EntryPoints
}
}
- private async void CreateRules(INatDevice device)
+ private Task CreateRules(INatDevice device)
{
if (_disposed)
{
@@ -153,50 +146,46 @@ namespace Emby.Server.Implementations.EntryPoints
// On some systems the device discovered event seems to fire repeatedly
// This check will help ensure we're not trying to port map the same device over and over
- var address = device.DeviceEndpoint;
-
- lock (_createdRulesLock)
+ if (!_createdRules.TryAdd(device.DeviceEndpoint, 0))
{
- if (!_createdRules.Contains(address))
- {
- _createdRules.Add(address);
- }
- else
- {
- return;
- }
+ return Task.CompletedTask;
}
- try
- {
- await CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error creating http port map");
- return;
- }
+ return Task.WhenAll(CreatePortMaps(device));
+ }
- try
- {
- await CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort).ConfigureAwait(false);
- }
- catch (Exception ex)
+ private IEnumerable<Task> CreatePortMaps(INatDevice device)
+ {
+ yield return CreatePortMap(device, _appHost.HttpPort, _config.Configuration.PublicPort);
+
+ if (_appHost.ListenWithHttps)
{
- _logger.LogError(ex, "Error creating https port map");
+ yield return CreatePortMap(device, _appHost.HttpsPort, _config.Configuration.PublicHttpsPort);
}
}
- private Task<Mapping> CreatePortMap(INatDevice device, int privatePort, int publicPort)
+ private async Task CreatePortMap(INatDevice device, int privatePort, int publicPort)
{
_logger.LogDebug(
- "Creating port map on local port {0} to public port {1} with device {2}",
+ "Creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}",
privatePort,
publicPort,
device.DeviceEndpoint);
- return device.CreatePortMapAsync(
- new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name));
+ try
+ {
+ var mapping = new Mapping(Protocol.Tcp, privatePort, publicPort, 0, _appHost.Name);
+ await device.CreatePortMapAsync(mapping).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(
+ ex,
+ "Error creating port map on local port {LocalPort} to public port {PublicPort} with device {DeviceEndpoint}.",
+ privatePort,
+ publicPort,
+ device.DeviceEndpoint);
+ }
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
index 8e9771931..2e738deeb 100644
--- a/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
+++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs
@@ -16,46 +16,63 @@ namespace Emby.Server.Implementations.EntryPoints
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>
- public StartupWizard(IServerApplicationHost appHost, IConfiguration appConfig, IServerConfigurationManager config)
+ /// <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 Task.CompletedTask;
+ return;
}
- if (!_appConfig.HostWebClient())
+ // Always launch the startup wizard if possible when it has not been completed
+ if (!_config.Configuration.IsStartupWizardCompleted && _appConfig.HostWebClient())
{
- BrowserLauncher.OpenSwaggerPage(_appHost);
+ BrowserLauncher.OpenWebApp(_appHost);
+ return;
+ }
+
+ // Do nothing if the web app is configured to not run automatically
+ if (!_config.Configuration.AutoRunWebApp || _startupOptions.NoAutoRunWebApp)
+ {
+ return;
}
- else if (!_config.Configuration.IsStartupWizardCompleted)
+
+ // Launch the swagger page if the web client is not hosted, otherwise open the web client
+ if (_appConfig.HostWebClient())
{
BrowserLauncher.OpenWebApp(_appHost);
}
- else if (_config.Configuration.AutoRunWebApp)
+ else
{
- var options = ((ApplicationHost)_appHost).StartupOptions;
-
- if (!options.NoAutoRunWebApp)
- {
- BrowserLauncher.OpenWebApp(_appHost);
- }
+ BrowserLauncher.OpenSwaggerPage(_appHost);
}
-
- return Task.CompletedTask;
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
index 50ba0f8fa..6929c81f9 100644
--- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
+++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Emby.Server.Implementations.Udp;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Plugins;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints
@@ -22,6 +23,7 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
+ private readonly IConfiguration _config;
/// <summary>
/// The UDP server.
@@ -35,18 +37,19 @@ namespace Emby.Server.Implementations.EntryPoints
/// </summary>
public UdpServerEntryPoint(
ILogger<UdpServerEntryPoint> logger,
- IServerApplicationHost appHost)
+ IServerApplicationHost appHost,
+ IConfiguration configuration)
{
_logger = logger;
_appHost = appHost;
-
+ _config = configuration;
}
/// <inheritdoc />
public async Task RunAsync()
{
- _udpServer = new UdpServer(_logger, _appHost);
+ _udpServer = new UdpServer(_logger, _appHost, _config);
_udpServer.Start(PortNumber, _cancellationTokenSource.Token);
}
diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
index 882bfe2f6..d66bb7638 100644
--- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
+++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs
@@ -6,6 +6,7 @@ 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;
@@ -24,7 +25,7 @@ namespace Emby.Server.Implementations.HttpClientManager
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem;
- private readonly Func<string> _defaultUserAgentFn;
+ 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.
@@ -40,12 +41,12 @@ namespace Emby.Server.Implementations.HttpClientManager
IApplicationPaths appPaths,
ILogger<HttpClientManager> logger,
IFileSystem fileSystem,
- Func<string> defaultUserAgentFn)
+ IApplicationHost appHost)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_fileSystem = fileSystem;
_appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths));
- _defaultUserAgentFn = defaultUserAgentFn;
+ _appHost = appHost;
}
/// <summary>
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.HttpClientManager
if (options.EnableDefaultUserAgent
&& !request.Headers.TryGetValues(HeaderNames.UserAgent, out _))
{
- request.Headers.Add(HeaderNames.UserAgent, _defaultUserAgentFn());
+ request.Headers.Add(HeaderNames.UserAgent, _appHost.ApplicationUserAgent);
}
switch (options.DecompressionMethod)
diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
index 5ae65a4e3..7de4f168c 100644
--- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs
@@ -6,14 +6,16 @@ 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.Net;
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;
@@ -21,15 +23,17 @@ 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, IDisposable
+ public class HttpListenerHost : IHttpServer
{
/// <summary>
/// The key for a setting that specifies the default redirect path
@@ -38,17 +42,17 @@ namespace Emby.Server.Implementations.HttpServer
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
private readonly ILogger _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 IHttpListener _socketListener;
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 List<IWebSocketConnection> _webSocketConnections = new List<IWebSocketConnection>();
private readonly IHostEnvironment _hostEnvironment;
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
@@ -62,10 +66,10 @@ namespace Emby.Server.Implementations.HttpServer
INetworkManager networkManager,
IJsonSerializer jsonSerializer,
IXmlSerializer xmlSerializer,
- IHttpListener socketListener,
ILocalizationManager localizationManager,
ServiceController serviceController,
- IHostEnvironment hostEnvironment)
+ IHostEnvironment hostEnvironment,
+ ILoggerFactory loggerFactory)
{
_appHost = applicationHost;
_logger = logger;
@@ -75,11 +79,9 @@ namespace Emby.Server.Implementations.HttpServer
_networkManager = networkManager;
_jsonSerializer = jsonSerializer;
_xmlSerializer = xmlSerializer;
- _socketListener = socketListener;
ServiceController = serviceController;
-
- _socketListener.WebSocketConnected = OnWebSocketConnected;
_hostEnvironment = hostEnvironment;
+ _loggerFactory = loggerFactory;
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
@@ -171,38 +173,6 @@ namespace Emby.Server.Implementations.HttpServer
return attributes;
}
- private void OnWebSocketConnected(WebSocketConnectEventArgs e)
- {
- if (_disposed)
- {
- return;
- }
-
- var connection = new WebSocketConnection(e.WebSocket, e.Endpoint, _jsonSerializer, _logger)
- {
- OnReceive = ProcessWebSocketMessageReceived,
- Url = e.Url,
- QueryString = e.QueryString
- };
-
- connection.Closed += OnConnectionClosed;
-
- lock (_webSocketConnections)
- {
- _webSocketConnections.Add(connection);
- }
-
- WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
- }
-
- private void OnConnectionClosed(object sender, EventArgs e)
- {
- lock (_webSocketConnections)
- {
- _webSocketConnections.Remove((IWebSocketConnection)sender);
- }
- }
-
private static Exception GetActualException(Exception ex)
{
if (ex is AggregateException agg)
@@ -230,7 +200,8 @@ namespace Emby.Server.Implementations.HttpServer
switch (ex)
{
case ArgumentException _: return 400;
- case SecurityException _: return 401;
+ case AuthenticationException _: return 401;
+ case SecurityException _: return 403;
case DirectoryNotFoundException _:
case FileNotFoundException _:
case ResourceNotFoundException _: return 404;
@@ -239,81 +210,44 @@ namespace Emby.Server.Implementations.HttpServer
}
}
- private async Task ErrorHandler(Exception ex, IRequest httpReq, bool logExceptionStackTrace, string urlToLog)
+ private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
{
- try
+ if (ignoreStackTrace)
{
- ex = GetActualException(ex);
-
- if (logExceptionStackTrace)
- {
- _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
- }
- else
- {
- _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
- }
-
- var httpRes = httpReq.Response;
-
- if (httpRes.HasStarted)
- {
- return;
- }
-
- var statusCode = GetStatusCode(ex);
- httpRes.StatusCode = statusCode;
-
- var errContent = NormalizeExceptionMessage(ex.Message);
- httpRes.ContentType = "text/plain";
- httpRes.ContentLength = errContent.Length;
- await httpRes.WriteAsync(errContent).ConfigureAwait(false);
+ _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
}
- catch (Exception errorEx)
+ else
{
- _logger.LogError(errorEx, "Error this.ProcessRequest(context)(Exception while writing error to the response). URL: {Url}", urlToLog);
+ _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
}
- }
- private string NormalizeExceptionMessage(string msg)
- {
- if (msg == null)
+ var httpRes = httpReq.Response;
+
+ if (httpRes.HasStarted)
{
- return string.Empty;
+ return;
}
- // Strip any information we don't want to reveal
-
- msg = msg.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase);
- msg = msg.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
+ httpRes.StatusCode = statusCode;
- return msg;
+ var errContent = NormalizeExceptionMessage(ex) ?? string.Empty;
+ httpRes.ContentType = "text/plain";
+ httpRes.ContentLength = errContent.Length;
+ await httpRes.WriteAsync(errContent).ConfigureAwait(false);
}
- /// <summary>
- /// Shut down the Web Service
- /// </summary>
- public void Stop()
+ private string NormalizeExceptionMessage(Exception ex)
{
- List<IWebSocketConnection> connections;
-
- lock (_webSocketConnections)
+ // Do not expose the exception message for AuthenticationException
+ if (ex is AuthenticationException)
{
- connections = _webSocketConnections.ToList();
- _webSocketConnections.Clear();
+ return null;
}
- foreach (var connection in connections)
- {
- try
- {
- connection.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error disposing connection");
- }
- }
+ // 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)
@@ -425,33 +359,52 @@ namespace Emby.Server.Implementations.HttpServer
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.EnableHttps && !_config.Configuration.IsBehindProxy)
+ if (_config.Configuration.RequireHttps
+ && _appHost.ListenWithHttps
+ && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
{
- if (urlString.IndexOf("https://", StringComparison.OrdinalIgnoreCase) == -1)
+ // 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)
{
- // 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;
- }
+ return true;
+ }
- if (!_networkManager.IsInLocalNetwork(remoteIp))
- {
- return false;
- }
+ 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, _logger);
+ return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
+ }
+
/// <summary>
/// Overridable method that can be used to implement a custom handler.
/// </summary>
- public async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
+ private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -494,9 +447,10 @@ namespace Emby.Server.Implementations.HttpServer
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
{
httpRes.StatusCode = 200;
- httpRes.Headers.Add("Access-Control-Allow-Origin", "*");
- httpRes.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- httpRes.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization");
+ 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;
@@ -536,22 +490,50 @@ namespace Emby.Server.Implementations.HttpServer
throw new FileNotFoundException();
}
}
- catch (Exception ex)
+ catch (Exception requestEx)
{
- // Do not handle exceptions manually when in development mode
- // The framework-defined development exception page will be returned instead
- if (_hostEnvironment.IsDevelopment())
+ try
{
- throw;
+ 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);
- bool ignoreStackTrace =
- ex is SocketException
- || ex is IOException
- || ex is OperationCanceledException
- || ex is SecurityException
- || ex is FileNotFoundException;
- await ErrorHandler(ex, httpReq, !ignoreStackTrace, urlToLog).ConfigureAwait(false);
+ if (_hostEnvironment.IsDevelopment())
+ {
+ throw aggregateEx;
+ }
+ }
}
finally
{
@@ -569,6 +551,68 @@ namespace Emby.Server.Implementations.HttpServer
}
}
+ 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);
+
+ 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)
{
@@ -615,7 +659,7 @@ namespace Emby.Server.Implementations.HttpServer
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
{
- new ResponseFilter(_logger).FilterResponse
+ new ResponseFilter(this, _logger).FilterResponse
};
}
@@ -676,11 +720,6 @@ namespace Emby.Server.Implementations.HttpServer
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
}
- public Task ProcessWebSocketRequest(HttpContext context)
- {
- return _socketListener.ProcessWebSocketRequest(context);
- }
-
private string NormalizeEmbyRoutePath(string path)
{
_logger.LogDebug("Normalizing /emby route");
@@ -699,28 +738,6 @@ namespace Emby.Server.Implementations.HttpServer
return _baseUrlPrefix + NormalizeUrlPath(path);
}
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- Stop();
- }
-
- _disposed = true;
- }
-
/// <summary>
/// Processes the web socket message received.
/// </summary>
@@ -732,8 +749,6 @@ namespace Emby.Server.Implementations.HttpServer
return Task.CompletedTask;
}
- _logger.LogDebug("Websocket message received: {0}", result.MessageType);
-
IEnumerable<Task> GetTasks()
{
foreach (var x in _webSocketListeners)
diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
index b42662420..2e9ecc4ae 100644
--- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
+++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs
@@ -28,6 +28,12 @@ namespace Emby.Server.Implementations.HttpServer
/// </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>
@@ -420,7 +426,11 @@ namespace Emby.Server.Implementations.HttpServer
if (!noCache)
{
- DateTime.TryParse(requestContext.Headers[HeaderNames.IfModifiedSince], out var ifModifiedSinceHeader);
+ 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))
{
@@ -629,7 +639,7 @@ namespace Emby.Server.Implementations.HttpServer
if (lastModifiedDate.HasValue)
{
- responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToString(CultureInfo.InvariantCulture);
+ responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture);
}
}
diff --git a/Emby.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs
deleted file mode 100644
index 501593725..000000000
--- a/Emby.Server.Implementations/HttpServer/IHttpListener.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.HttpServer
-{
- public interface IHttpListener : IDisposable
- {
- /// <summary>
- /// Gets or sets the error handler.
- /// </summary>
- /// <value>The error handler.</value>
- Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
- /// <summary>
- /// Gets or sets the request handler.
- /// </summary>
- /// <value>The request handler.</value>
- Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
- /// <summary>
- /// Gets or sets the web socket handler.
- /// </summary>
- /// <value>The web socket handler.</value>
- Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
- /// <summary>
- /// Stops this instance.
- /// </summary>
- Task Stop();
-
- Task ProcessWebSocketRequest(HttpContext ctx);
- }
-}
diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
index 5e0466629..85c3db9b2 100644
--- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
+++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
using System.Text;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -13,14 +15,17 @@ namespace Emby.Server.Implementations.HttpServer
/// </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(ILogger logger)
+ public ResponseFilter(IHttpServer server, ILogger logger)
{
+ _server = server;
_logger = logger;
}
@@ -32,10 +37,16 @@ namespace Emby.Server.Implementations.HttpServer
/// <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.Add("Access-Control-Allow-Headers", "Accept, Accept-Language, Authorization, Cache-Control, Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, Content-Type, 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");
- res.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
- res.Headers.Add("Access-Control-Allow-Origin", "*");
+ 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)
{
@@ -82,6 +93,10 @@ namespace Emby.Server.Implementations.HttpServer
{
return null;
}
+ else if (inString.Length == 0)
+ {
+ return inString;
+ }
var newString = new StringBuilder(inString.Length);
diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
index 58421aaf1..256b24924 100644
--- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
+++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs
@@ -2,6 +2,7 @@
using System;
using System.Linq;
+using System.Security.Authentication;
using Emby.Server.Implementations.SocketSharp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
@@ -68,7 +69,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user == null && auth.UserId != Guid.Empty)
{
- throw new SecurityException("User with Id " + auth.UserId + " not found");
+ throw new AuthenticationException("User with Id " + auth.UserId + " not found");
}
if (user != null)
@@ -108,18 +109,12 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (user.Policy.IsDisabled)
{
- throw new SecurityException("User account has been disabled.")
- {
- SecurityExceptionType = SecurityExceptionType.Unauthenticated
- };
+ throw new SecurityException("User account has been disabled.");
}
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
{
- throw new SecurityException("User account has been disabled.")
- {
- SecurityExceptionType = SecurityExceptionType.Unauthenticated
- };
+ throw new SecurityException("User account has been disabled.");
}
if (!user.Policy.IsAdministrator
@@ -128,10 +123,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
- throw new SecurityException("This user account is not allowed access at this time.")
- {
- SecurityExceptionType = SecurityExceptionType.ParentalControl
- };
+ throw new SecurityException("This user account is not allowed access at this time.");
}
}
@@ -190,10 +182,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (user == null || !user.Policy.IsAdministrator)
{
- throw new SecurityException("User does not have admin access.")
- {
- SecurityExceptionType = SecurityExceptionType.Unauthenticated
- };
+ throw new SecurityException("User does not have admin access.");
}
}
@@ -201,10 +190,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (user == null || !user.Policy.EnableContentDeletion)
{
- throw new SecurityException("User does not have delete access.")
- {
- SecurityExceptionType = SecurityExceptionType.Unauthenticated
- };
+ throw new SecurityException("User does not have delete access.");
}
}
@@ -212,10 +198,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (user == null || !user.Policy.EnableContentDownloading)
{
- throw new SecurityException("User does not have download access.")
- {
- SecurityExceptionType = SecurityExceptionType.Unauthenticated
- };
+ throw new SecurityException("User does not have download access.");
}
}
}
@@ -230,14 +213,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (string.IsNullOrEmpty(token))
{
- throw new SecurityException("Access token is required.");
+ throw new AuthenticationException("Access token is required.");
}
var info = GetTokenInfo(request);
if (info == null)
{
- throw new SecurityException("Access token is invalid or expired.");
+ throw new AuthenticationException("Access token is invalid or expired.");
}
//if (!string.IsNullOrEmpty(info.UserId))
diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
index 2292d86a4..0680c5ffe 100644
--- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
+++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
@@ -1,15 +1,18 @@
-using System;
+#nullable enable
+
+using System;
+using System.Buffers;
+using System.IO.Pipelines;
+using System.Net;
using System.Net.WebSockets;
-using System.Text;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
+using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
-using UtfUnknown;
namespace Emby.Server.Implementations.HttpServer
{
@@ -24,69 +27,50 @@ namespace Emby.Server.Implementations.HttpServer
private readonly ILogger _logger;
/// <summary>
- /// The json serializer.
+ /// The json serializer options.
/// </summary>
- private readonly IJsonSerializer _jsonSerializer;
+ private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// The socket.
/// </summary>
- private readonly IWebSocket _socket;
+ private readonly WebSocket _socket;
/// <summary>
/// Initializes a new instance of the <see cref="WebSocketConnection" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="socket">The socket.</param>
/// <param name="remoteEndPoint">The remote end point.</param>
- /// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="logger">The logger.</param>
- /// <exception cref="ArgumentNullException">socket</exception>
- public WebSocketConnection(IWebSocket socket, string remoteEndPoint, IJsonSerializer jsonSerializer, ILogger logger)
+ /// <param name="query">The query.</param>
+ public WebSocketConnection(
+ ILogger<WebSocketConnection> logger,
+ WebSocket socket,
+ IPAddress? remoteEndPoint,
+ IQueryCollection query)
{
- if (socket == null)
- {
- throw new ArgumentNullException(nameof(socket));
- }
-
- if (string.IsNullOrEmpty(remoteEndPoint))
- {
- throw new ArgumentNullException(nameof(remoteEndPoint));
- }
-
- if (jsonSerializer == null)
- {
- throw new ArgumentNullException(nameof(jsonSerializer));
- }
-
- if (logger == null)
- {
- throw new ArgumentNullException(nameof(logger));
- }
-
- Id = Guid.NewGuid();
- _jsonSerializer = jsonSerializer;
+ _logger = logger;
_socket = socket;
- _socket.OnReceiveBytes = OnReceiveInternal;
-
RemoteEndPoint = remoteEndPoint;
- _logger = logger;
+ QueryString = query;
- socket.Closed += OnSocketClosed;
+ _jsonOptions = JsonDefaults.GetOptions();
+ LastActivityDate = DateTime.Now;
}
/// <inheritdoc />
- public event EventHandler<EventArgs> Closed;
+ public event EventHandler<EventArgs>? Closed;
/// <summary>
/// Gets or sets the remote end point.
/// </summary>
- public string RemoteEndPoint { get; private set; }
+ public IPAddress? RemoteEndPoint { get; }
/// <summary>
/// Gets or sets the receive action.
/// </summary>
/// <value>The receive action.</value>
- public Func<WebSocketMessageInfo, Task> OnReceive { get; set; }
+ public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
/// <summary>
/// Gets the last activity date.
@@ -94,23 +78,14 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The last activity date.</value>
public DateTime LastActivityDate { get; private set; }
- /// <summary>
- /// Gets the id.
- /// </summary>
- /// <value>The id.</value>
- public Guid Id { get; private set; }
-
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
+ /// <inheritdoc />
+ public DateTime LastKeepAliveDate { get; set; }
/// <summary>
/// Gets or sets the query string.
/// </summary>
/// <value>The query string.</value>
- public IQueryCollection QueryString { get; set; }
+ public IQueryCollection QueryString { get; }
/// <summary>
/// Gets the state.
@@ -118,119 +93,151 @@ namespace Emby.Server.Implementations.HttpServer
/// <value>The state.</value>
public WebSocketState State => _socket.State;
- void OnSocketClosed(object sender, EventArgs e)
+ /// <summary>
+ /// Sends a message asynchronously.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="message">The message.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
{
- Closed?.Invoke(this, EventArgs.Empty);
+ var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+ return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
}
- /// <summary>
- /// Called when [receive].
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- private void OnReceiveInternal(byte[] bytes)
+ /// <inheritdoc />
+ public async Task ProcessAsync(CancellationToken cancellationToken = default)
{
- LastActivityDate = DateTime.UtcNow;
+ var pipe = new Pipe();
+ var writer = pipe.Writer;
- if (OnReceive == null)
+ ValueWebSocketReceiveResult receiveresult;
+ do
{
- return;
- }
- var charset = CharsetDetector.DetectFromBytes(bytes).Detected?.EncodingName;
+ // Allocate at least 512 bytes from the PipeWriter
+ Memory<byte> memory = writer.GetMemory(512);
+ try
+ {
+ receiveresult = await _socket.ReceiveAsync(memory, cancellationToken);
+ }
+ catch (WebSocketException ex)
+ {
+ _logger.LogWarning("WS {IP} error receiving data: {Message}", RemoteEndPoint, ex.Message);
+ break;
+ }
- if (string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase))
- {
- OnReceiveInternal(Encoding.UTF8.GetString(bytes, 0, bytes.Length));
- }
- else
+ int bytesRead = receiveresult.Count;
+ if (bytesRead == 0)
+ {
+ break;
+ }
+
+ // Tell the PipeWriter how much was read from the Socket
+ writer.Advance(bytesRead);
+
+ // Make the data available to the PipeReader
+ FlushResult flushResult = await writer.FlushAsync();
+ if (flushResult.IsCompleted)
+ {
+ // The PipeReader stopped reading
+ break;
+ }
+
+ LastActivityDate = DateTime.UtcNow;
+
+ if (receiveresult.EndOfMessage)
+ {
+ await ProcessInternal(pipe.Reader).ConfigureAwait(false);
+ }
+ } while (
+ (_socket.State == WebSocketState.Open || _socket.State == WebSocketState.Connecting)
+ && receiveresult.MessageType != WebSocketMessageType.Close);
+
+ Closed?.Invoke(this, EventArgs.Empty);
+
+ if (_socket.State == WebSocketState.Open
+ || _socket.State == WebSocketState.CloseReceived
+ || _socket.State == WebSocketState.CloseSent)
{
- OnReceiveInternal(Encoding.ASCII.GetString(bytes, 0, bytes.Length));
+ await _socket.CloseAsync(
+ WebSocketCloseStatus.NormalClosure,
+ string.Empty,
+ cancellationToken).ConfigureAwait(false);
}
}
- private void OnReceiveInternal(string message)
+ private async Task ProcessInternal(PipeReader reader)
{
- LastActivityDate = DateTime.UtcNow;
-
- if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
- {
- // This info is useful sometimes but also clogs up the log
- _logger.LogDebug("Received web socket message that is not a json structure: {message}", message);
- return;
- }
+ ReadResult result = await reader.ReadAsync().ConfigureAwait(false);
+ ReadOnlySequence<byte> buffer = result.Buffer;
if (OnReceive == null)
{
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
return;
}
+ WebSocketMessage<object> stub;
try
{
- var stub = (WebSocketMessage<object>)_jsonSerializer.DeserializeFromString(message, typeof(WebSocketMessage<object>));
- var info = new WebSocketMessageInfo
+ if (buffer.IsSingleSegment)
{
- MessageType = stub.MessageType,
- Data = stub.Data?.ToString(),
- Connection = this
- };
-
- OnReceive(info);
+ stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buffer.FirstSpan, _jsonOptions);
+ }
+ else
+ {
+ var buf = ArrayPool<byte>.Shared.Rent(Convert.ToInt32(buffer.Length));
+ try
+ {
+ buffer.CopyTo(buf);
+ stub = JsonSerializer.Deserialize<WebSocketMessage<object>>(buf, _jsonOptions);
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buf);
+ }
+ }
}
- catch (Exception ex)
+ catch (JsonException ex)
{
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
_logger.LogError(ex, "Error processing web socket message");
+ return;
}
- }
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <typeparam name="T"></typeparam>
- /// <param name="message">The message.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- /// <exception cref="ArgumentNullException">message</exception>
- public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
- {
- if (message == null)
- {
- throw new ArgumentNullException(nameof(message));
- }
+ // Tell the PipeReader how much of the buffer we have consumed
+ reader.AdvanceTo(buffer.End);
- var json = _jsonSerializer.SerializeToString(message);
+ _logger.LogDebug("WS {IP} received message: {@Message}", RemoteEndPoint, stub);
- return SendAsync(json, cancellationToken);
- }
+ var info = new WebSocketMessageInfo
+ {
+ MessageType = stub.MessageType,
+ Data = stub.Data?.ToString(), // Data can be null
+ Connection = this
+ };
- /// <summary>
- /// Sends a message asynchronously.
- /// </summary>
- /// <param name="buffer">The buffer.</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(byte[] buffer, CancellationToken cancellationToken)
- {
- if (buffer == null)
+ if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal))
{
- throw new ArgumentNullException(nameof(buffer));
+ await SendKeepAliveResponse();
+ }
+ else
+ {
+ await OnReceive(info).ConfigureAwait(false);
}
-
- cancellationToken.ThrowIfCancellationRequested();
-
- return _socket.SendAsync(buffer, true, cancellationToken);
}
- /// <inheritdoc />
- public Task SendAsync(string text, CancellationToken cancellationToken)
+ private Task SendKeepAliveResponse()
{
- if (string.IsNullOrEmpty(text))
+ LastKeepAliveDate = DateTime.UtcNow;
+ return SendAsync(new WebSocketMessage<string>
{
- throw new ArgumentNullException(nameof(text));
- }
-
- cancellationToken.ThrowIfCancellationRequested();
-
- return _socket.SendAsync(text, true, cancellationToken);
+ MessageType = "KeepAlive"
+ }, CancellationToken.None);
}
/// <inheritdoc />
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index b1fb8cc63..eb5e190aa 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -11,12 +11,18 @@ 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
{
public class LibraryMonitor : ILibraryMonitor
{
+ private readonly ILogger _logger;
+ private readonly ILibraryManager _libraryManager;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IFileSystem _fileSystem;
+
/// <summary>
/// The file system watchers.
/// </summary>
@@ -33,38 +39,6 @@ namespace Emby.Server.Implementations.IO
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
/// <summary>
- /// Any file name ending in any of these will be ignored by the watchers.
- /// </summary>
- private static readonly HashSet<string> _alwaysIgnoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- "small.jpg",
- "albumart.jpg",
-
- // WMC temp recording directories that will constantly be written to
- "TempRec",
- "TempSBE"
- };
-
- private static readonly string[] _alwaysIgnoreSubstrings = new string[]
- {
- // Synology
- "eaDir",
- "#recycle",
- ".wd_tv",
- ".actors"
- };
-
- private static readonly HashSet<string> _alwaysIgnoreExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
- {
- // thumbs.db
- ".db",
-
- // bts sync files
- ".bts",
- ".sync"
- };
-
- /// <summary>
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
/// </summary>
/// <param name="path">The path.</param>
@@ -113,34 +87,23 @@ 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);
}
}
}
/// <summary>
- /// Gets or sets the logger.
- /// </summary>
- /// <value>The logger.</value>
- private ILogger Logger { get; set; }
-
- private ILibraryManager LibraryManager { get; set; }
- private IServerConfigurationManager ConfigurationManager { get; set; }
-
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
/// </summary>
public LibraryMonitor(
- ILoggerFactory loggerFactory,
+ ILogger<LibraryMonitor> logger,
ILibraryManager libraryManager,
IServerConfigurationManager configurationManager,
IFileSystem fileSystem)
{
- LibraryManager = libraryManager;
- Logger = loggerFactory.CreateLogger(GetType().Name);
- ConfigurationManager = configurationManager;
+ _libraryManager = libraryManager;
+ _logger = logger;
+ _configurationManager = configurationManager;
_fileSystem = fileSystem;
}
@@ -151,7 +114,7 @@ namespace Emby.Server.Implementations.IO
return false;
}
- var options = LibraryManager.GetLibraryOptions(item);
+ var options = _libraryManager.GetLibraryOptions(item);
if (options != null)
{
@@ -163,12 +126,12 @@ namespace Emby.Server.Implementations.IO
public void Start()
{
- LibraryManager.ItemAdded += OnLibraryManagerItemAdded;
- LibraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
+ _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
+ _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
var pathsToWatch = new List<string>();
- var paths = LibraryManager
+ var paths = _libraryManager
.RootFolder
.Children
.Where(IsLibraryMonitorEnabled)
@@ -261,7 +224,7 @@ namespace Emby.Server.Implementations.IO
if (!Directory.Exists(path))
{
// Seeing a crash in the mono runtime due to an exception being thrown on a different thread
- Logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
+ _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
return;
}
@@ -297,7 +260,7 @@ namespace Emby.Server.Implementations.IO
if (_fileSystemWatchers.TryAdd(path, newWatcher))
{
newWatcher.EnableRaisingEvents = true;
- Logger.LogInformation("Watching directory " + path);
+ _logger.LogInformation("Watching directory " + path);
}
else
{
@@ -307,7 +270,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error watching path: {path}", path);
+ _logger.LogError(ex, "Error watching path: {path}", path);
}
});
}
@@ -333,7 +296,7 @@ namespace Emby.Server.Implementations.IO
{
using (watcher)
{
- Logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
+ _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
watcher.Created -= OnWatcherChanged;
watcher.Deleted -= OnWatcherChanged;
@@ -372,7 +335,7 @@ namespace Emby.Server.Implementations.IO
var ex = e.GetException();
var dw = (FileSystemWatcher)sender;
- Logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
+ _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
DisposeWatcher(dw, true);
}
@@ -390,7 +353,7 @@ namespace Emby.Server.Implementations.IO
}
catch (Exception ex)
{
- Logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
+ _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
}
}
@@ -401,12 +364,7 @@ namespace Emby.Server.Implementations.IO
throw new ArgumentNullException(nameof(path));
}
- var filename = Path.GetFileName(path);
-
- var monitorPath = !string.IsNullOrEmpty(filename) &&
- !_alwaysIgnoreFiles.Contains(filename) &&
- !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path)) &&
- _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1);
+ var monitorPath = !IgnorePatterns.ShouldIgnore(path);
// Ignore certain files
var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
@@ -416,13 +374,13 @@ namespace Emby.Server.Implementations.IO
{
if (_fileSystem.AreEqual(i, path))
{
- Logger.LogDebug("Ignoring change to {Path}", path);
+ _logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
if (_fileSystem.ContainsSubPath(i, path))
{
- Logger.LogDebug("Ignoring change to {Path}", path);
+ _logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
@@ -430,7 +388,7 @@ namespace Emby.Server.Implementations.IO
var parent = Path.GetDirectoryName(i);
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
- Logger.LogDebug("Ignoring change to {Path}", path);
+ _logger.LogDebug("Ignoring change to {Path}", path);
return true;
}
@@ -485,7 +443,7 @@ namespace Emby.Server.Implementations.IO
}
}
- var newRefresher = new FileRefresher(path, ConfigurationManager, LibraryManager, Logger);
+ var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
newRefresher.Completed += NewRefresher_Completed;
_activeRefreshers.Add(newRefresher);
}
@@ -502,8 +460,8 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public void Stop()
{
- LibraryManager.ItemAdded -= OnLibraryManagerItemAdded;
- LibraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
+ _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
+ _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
foreach (var watcher in _fileSystemWatchers.Values.ToList())
{
diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs
index 16b68170b..acae702f3 100644
--- a/Emby.Server.Implementations/IStartupOptions.cs
+++ b/Emby.Server.Implementations/IStartupOptions.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace Emby.Server.Implementations
{
public interface IStartupOptions
@@ -36,5 +38,10 @@ namespace Emby.Server.Implementations
/// Gets the value of the --plugin-manifest-url command line option.
/// </summary>
string PluginManifestUrl { get; }
+
+ /// <summary>
+ /// Gets the value of the --published-server-url command line option.
+ /// </summary>
+ Uri PublishedServerUrl { get; }
}
}
diff --git a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
index bc1398332..218e5a0c6 100644
--- a/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
+++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
@@ -1,7 +1,5 @@
using System;
using System.IO;
-using System.Linq;
-using System.Text.RegularExpressions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Resolvers;
@@ -17,32 +15,6 @@ namespace Emby.Server.Implementations.Library
private readonly ILibraryManager _libraryManager;
/// <summary>
- /// Any folder named in this list will be ignored
- /// </summary>
- private static readonly string[] _ignoreFolders =
- {
- "metadata",
- "ps3_update",
- "ps3_vprm",
- "extrafanart",
- "extrathumbs",
- ".actors",
- ".wd_tv",
-
- // Synology
- "@eaDir",
- "eaDir",
- "#recycle",
-
- // Qnap
- "@Recycle",
- ".@__thumb",
- "$RECYCLE.BIN",
- "System Volume Information",
- ".grab",
- };
-
- /// <summary>
/// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
/// </summary>
/// <param name="libraryManager">The library manager.</param>
@@ -60,23 +32,15 @@ namespace Emby.Server.Implementations.Library
return false;
}
- var filename = fileInfo.Name;
-
- // Ignore hidden files on UNIX
- if (Environment.OSVersion.Platform != PlatformID.Win32NT
- && filename[0] == '.')
+ if (IgnorePatterns.ShouldIgnore(fileInfo.FullName))
{
return true;
}
+ var filename = fileInfo.Name;
+
if (fileInfo.IsDirectory)
{
- // Ignore any folders in our list
- if (_ignoreFolders.Contains(filename, StringComparer.OrdinalIgnoreCase))
- {
- return true;
- }
-
if (parent != null)
{
// Ignore trailer folders but allow it at the collection level
@@ -109,11 +73,6 @@ namespace Emby.Server.Implementations.Library
return true;
}
}
-
- // Ignore samples
- Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase);
-
- return m.Success;
}
return false;
diff --git a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
index ab036eca7..52c8facc3 100644
--- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
+++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library
{
if (resolvedUser == null)
{
- throw new ArgumentNullException(nameof(resolvedUser));
+ throw new AuthenticationException($"Specified user does not exist.");
}
bool success = false;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
new file mode 100644
index 000000000..d12b5855b
--- /dev/null
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -0,0 +1,74 @@
+using System.Linq;
+using DotNet.Globbing;
+
+namespace Emby.Server.Implementations.Library
+{
+ /// <summary>
+ /// Glob patterns for files to ignore
+ /// </summary>
+ public static class IgnorePatterns
+ {
+ /// <summary>
+ /// Files matching these glob patterns will be ignored
+ /// </summary>
+ public static readonly string[] Patterns = new string[]
+ {
+ "**/small.jpg",
+ "**/albumart.jpg",
+ "**/*sample*",
+
+ // Directories
+ "**/metadata/**",
+ "**/ps3_update/**",
+ "**/ps3_vprm/**",
+ "**/extrafanart/**",
+ "**/extrathumbs/**",
+ "**/.actors/**",
+ "**/.wd_tv/**",
+ "**/lost+found/**",
+
+ // WMC temp recording directories that will constantly be written to
+ "**/TempRec/**",
+ "**/TempSBE/**",
+
+ // Synology
+ "**/eaDir/**",
+ "**/@eaDir/**",
+ "**/#recycle/**",
+
+ // Qnap
+ "**/@Recycle/**",
+ "**/.@__thumb/**",
+ "**/$RECYCLE.BIN/**",
+ "**/System Volume Information/**",
+ "**/.grab/**",
+
+ // Unix hidden files and directories
+ "**/.*/**",
+
+ // thumbs.db
+ "**/thumbs.db",
+
+ // bts sync files
+ "**/*.bts",
+ "**/*.sync",
+ };
+
+ private static readonly GlobOptions _globOptions = new GlobOptions
+ {
+ Evaluation = {
+ CaseInsensitive = true
+ }
+ };
+
+ private static readonly Glob[] _globs = Patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
+
+ /// <summary>
+ /// Returns true if the supplied path should be ignored
+ /// </summary>
+ public static bool ShouldIgnore(string path)
+ {
+ return _globs.Any(g => g.IsMatch(path));
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 4e2cf334c..0b86b2db7 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -54,9 +54,29 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public class LibraryManager : ILibraryManager
{
+ private readonly ILogger _logger;
+ private readonly ITaskManager _taskManager;
+ private readonly IUserManager _userManager;
+ private readonly IUserDataManager _userDataRepository;
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly Lazy<ILibraryMonitor> _libraryMonitorFactory;
+ private readonly Lazy<IProviderManager> _providerManagerFactory;
+ private readonly Lazy<IUserViewManager> _userviewManagerFactory;
+ private readonly IServerApplicationHost _appHost;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly IFileSystem _fileSystem;
+ private readonly IItemRepository _itemRepository;
+ private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
+
private NamingOptions _namingOptions;
private string[] _videoFileExtensions;
+ private ILibraryMonitor LibraryMonitor => _libraryMonitorFactory.Value;
+
+ private IProviderManager ProviderManager => _providerManagerFactory.Value;
+
+ private IUserViewManager UserViewManager => _userviewManagerFactory.Value;
+
/// <summary>
/// Gets or sets the postscan tasks.
/// </summary>
@@ -90,12 +110,6 @@ namespace Emby.Server.Implementations.Library
private IBaseItemComparer[] Comparers { get; set; }
/// <summary>
- /// Gets or sets the active item repository
- /// </summary>
- /// <value>The item repository.</value>
- public IItemRepository ItemRepository { get; set; }
-
- /// <summary>
/// Occurs when [item added].
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemAdded;
@@ -110,90 +124,47 @@ namespace Emby.Server.Implementations.Library
/// </summary>
public event EventHandler<ItemChangeEventArgs> ItemRemoved;
- /// <summary>
- /// The _logger
- /// </summary>
- private readonly ILogger _logger;
-
- /// <summary>
- /// The _task manager
- /// </summary>
- private readonly ITaskManager _taskManager;
-
- /// <summary>
- /// The _user manager
- /// </summary>
- private readonly IUserManager _userManager;
-
- /// <summary>
- /// The _user data repository
- /// </summary>
- private readonly IUserDataManager _userDataRepository;
-
- /// <summary>
- /// Gets or sets the configuration manager.
- /// </summary>
- /// <value>The configuration manager.</value>
- private IServerConfigurationManager ConfigurationManager { get; set; }
-
- private readonly Func<ILibraryMonitor> _libraryMonitorFactory;
- private readonly Func<IProviderManager> _providerManagerFactory;
- private readonly Func<IUserViewManager> _userviewManager;
public bool IsScanRunning { get; private set; }
- private IServerApplicationHost _appHost;
- private readonly IMediaEncoder _mediaEncoder;
-
- /// <summary>
- /// The _library items cache
- /// </summary>
- private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
-
- /// <summary>
- /// Gets the library items cache.
- /// </summary>
- /// <value>The library items cache.</value>
- private ConcurrentDictionary<Guid, BaseItem> LibraryItemsCache => _libraryItemsCache;
-
- private readonly IFileSystem _fileSystem;
-
/// <summary>
/// Initializes a new instance of the <see cref="LibraryManager" /> class.
/// </summary>
/// <param name="appHost">The application host</param>
- /// <param name="loggerFactory">The logger factory.</param>
+ /// <param name="logger">The logger.</param>
/// <param name="taskManager">The task manager.</param>
/// <param name="userManager">The user manager.</param>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="userDataRepository">The user data repository.</param>
public LibraryManager(
IServerApplicationHost appHost,
- ILoggerFactory loggerFactory,
+ ILogger<LibraryManager> logger,
ITaskManager taskManager,
IUserManager userManager,
IServerConfigurationManager configurationManager,
IUserDataManager userDataRepository,
- Func<ILibraryMonitor> libraryMonitorFactory,
+ Lazy<ILibraryMonitor> libraryMonitorFactory,
IFileSystem fileSystem,
- Func<IProviderManager> providerManagerFactory,
- Func<IUserViewManager> userviewManager,
- IMediaEncoder mediaEncoder)
+ Lazy<IProviderManager> providerManagerFactory,
+ Lazy<IUserViewManager> userviewManagerFactory,
+ IMediaEncoder mediaEncoder,
+ IItemRepository itemRepository)
{
_appHost = appHost;
- _logger = loggerFactory.CreateLogger(nameof(LibraryManager));
+ _logger = logger;
_taskManager = taskManager;
_userManager = userManager;
- ConfigurationManager = configurationManager;
+ _configurationManager = configurationManager;
_userDataRepository = userDataRepository;
_libraryMonitorFactory = libraryMonitorFactory;
_fileSystem = fileSystem;
_providerManagerFactory = providerManagerFactory;
- _userviewManager = userviewManager;
+ _userviewManagerFactory = userviewManagerFactory;
_mediaEncoder = mediaEncoder;
+ _itemRepository = itemRepository;
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
- ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated;
+ _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
RecordConfigurationValues(configurationManager.Configuration);
}
@@ -272,7 +243,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
private void ConfigurationUpdated(object sender, EventArgs e)
{
- var config = ConfigurationManager.Configuration;
+ var config = _configurationManager.Configuration;
var wizardChanged = config.IsStartupWizardCompleted != _wizardCompleted;
@@ -306,7 +277,7 @@ namespace Emby.Server.Implementations.Library
}
}
- LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
+ _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -437,10 +408,10 @@ namespace Emby.Server.Implementations.Library
item.SetParent(null);
- ItemRepository.DeleteItem(item.Id);
+ _itemRepository.DeleteItem(item.Id);
foreach (var child in children)
{
- ItemRepository.DeleteItem(child.Id);
+ _itemRepository.DeleteItem(child.Id);
}
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
@@ -509,15 +480,15 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(type));
}
- if (key.StartsWith(ConfigurationManager.ApplicationPaths.ProgramDataPath, StringComparison.Ordinal))
+ if (key.StartsWith(_configurationManager.ApplicationPaths.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(_configurationManager.ApplicationPaths.ProgramDataPath.Length)
.TrimStart(new[] { '/', '\\' })
.Replace("/", "\\");
}
- if (forceCaseInsensitive || !ConfigurationManager.Configuration.EnableCaseSensitiveItemIds)
+ if (forceCaseInsensitive || !_configurationManager.Configuration.EnableCaseSensitiveItemIds)
{
key = key.ToLowerInvariant();
}
@@ -550,7 +521,7 @@ namespace Emby.Server.Implementations.Library
collectionType = GetContentTypeOverride(fullPath, true);
}
- var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, directoryService)
+ var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
{
Parent = parent,
Path = fullPath,
@@ -720,7 +691,7 @@ namespace Emby.Server.Implementations.Library
/// <exception cref="InvalidOperationException">Cannot create the root folder until plugins have loaded.</exception>
public AggregateFolder CreateRootFolder()
{
- var rootFolderPath = ConfigurationManager.ApplicationPaths.RootFolderPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.RootFolderPath;
Directory.CreateDirectory(rootFolderPath);
@@ -734,7 +705,7 @@ namespace Emby.Server.Implementations.Library
}
// Add in the plug-in folders
- var path = Path.Combine(ConfigurationManager.ApplicationPaths.DataPath, "playlists");
+ var path = Path.Combine(_configurationManager.ApplicationPaths.DataPath, "playlists");
Directory.CreateDirectory(path);
@@ -786,7 +757,7 @@ namespace Emby.Server.Implementations.Library
{
if (_userRootFolder == null)
{
- var userRootPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
_logger.LogDebug("Creating userRootPath at {path}", userRootPath);
Directory.CreateDirectory(userRootPath);
@@ -980,7 +951,7 @@ namespace Emby.Server.Implementations.Library
where T : BaseItem, new()
{
var path = getPathFn(name);
- var forceCaseInsensitiveId = ConfigurationManager.Configuration.EnableNormalizedItemByNameIds;
+ var forceCaseInsensitiveId = _configurationManager.Configuration.EnableNormalizedItemByNameIds;
return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId);
}
@@ -994,7 +965,7 @@ namespace Emby.Server.Implementations.Library
public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
{
// Ensure the location is available.
- Directory.CreateDirectory(ConfigurationManager.ApplicationPaths.PeoplePath);
+ Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath);
return new PeopleValidator(this, _logger, _fileSystem).ValidatePeople(cancellationToken, progress);
}
@@ -1031,7 +1002,7 @@ namespace Emby.Server.Implementations.Library
public async Task ValidateMediaLibraryInternal(IProgress<double> progress, CancellationToken cancellationToken)
{
IsScanRunning = true;
- _libraryMonitorFactory().Stop();
+ LibraryMonitor.Stop();
try
{
@@ -1039,7 +1010,7 @@ namespace Emby.Server.Implementations.Library
}
finally
{
- _libraryMonitorFactory().Start();
+ LibraryMonitor.Start();
IsScanRunning = false;
}
}
@@ -1148,7 +1119,7 @@ namespace Emby.Server.Implementations.Library
progress.Report(percent * 100);
}
- ItemRepository.UpdateInheritedValues(cancellationToken);
+ _itemRepository.UpdateInheritedValues(cancellationToken);
progress.Report(100);
}
@@ -1168,9 +1139,9 @@ namespace Emby.Server.Implementations.Library
var topLibraryFolders = GetUserRootFolder().Children.ToList();
_logger.LogDebug("Getting refreshQueue");
- var refreshQueue = includeRefreshState ? _providerManagerFactory().GetRefreshQueue() : null;
+ var refreshQueue = includeRefreshState ? ProviderManager.GetRefreshQueue() : null;
- return _fileSystem.GetDirectoryPaths(ConfigurationManager.ApplicationPaths.DefaultUserViewsPath)
+ return _fileSystem.GetDirectoryPaths(_configurationManager.ApplicationPaths.DefaultUserViewsPath)
.Select(dir => GetVirtualFolderInfo(dir, topLibraryFolders, refreshQueue))
.ToList();
}
@@ -1245,7 +1216,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
- if (LibraryItemsCache.TryGetValue(id, out BaseItem item))
+ if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
{
return item;
}
@@ -1276,7 +1247,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
- return ItemRepository.GetItemList(query);
+ return _itemRepository.GetItemList(query);
}
public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1300,7 +1271,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
- return ItemRepository.GetCount(query);
+ return _itemRepository.GetCount(query);
}
public List<BaseItem> GetItemList(InternalItemsQuery query, List<BaseItem> parents)
@@ -1315,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
}
}
- return ItemRepository.GetItemList(query);
+ return _itemRepository.GetItemList(query);
}
public QueryResult<BaseItem> QueryItems(InternalItemsQuery query)
@@ -1327,12 +1298,12 @@ namespace Emby.Server.Implementations.Library
if (query.EnableTotalRecordCount)
{
- return ItemRepository.GetItems(query);
+ return _itemRepository.GetItems(query);
}
return new QueryResult<BaseItem>
{
- Items = ItemRepository.GetItemList(query).ToArray()
+ Items = _itemRepository.GetItemList(query).ToArray()
};
}
@@ -1343,7 +1314,7 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User);
}
- return ItemRepository.GetItemIdsList(query);
+ return _itemRepository.GetItemIdsList(query);
}
public QueryResult<(BaseItem, ItemCounts)> GetStudios(InternalItemsQuery query)
@@ -1354,7 +1325,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetStudios(query);
+ return _itemRepository.GetStudios(query);
}
public QueryResult<(BaseItem, ItemCounts)> GetGenres(InternalItemsQuery query)
@@ -1365,7 +1336,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetGenres(query);
+ return _itemRepository.GetGenres(query);
}
public QueryResult<(BaseItem, ItemCounts)> GetMusicGenres(InternalItemsQuery query)
@@ -1376,7 +1347,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetMusicGenres(query);
+ return _itemRepository.GetMusicGenres(query);
}
public QueryResult<(BaseItem, ItemCounts)> GetAllArtists(InternalItemsQuery query)
@@ -1387,7 +1358,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetAllArtists(query);
+ return _itemRepository.GetAllArtists(query);
}
public QueryResult<(BaseItem, ItemCounts)> GetArtists(InternalItemsQuery query)
@@ -1398,7 +1369,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetArtists(query);
+ return _itemRepository.GetArtists(query);
}
private void SetTopParentOrAncestorIds(InternalItemsQuery query)
@@ -1439,7 +1410,7 @@ namespace Emby.Server.Implementations.Library
}
SetTopParentOrAncestorIds(query);
- return ItemRepository.GetAlbumArtists(query);
+ return _itemRepository.GetAlbumArtists(query);
}
public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
@@ -1460,10 +1431,10 @@ namespace Emby.Server.Implementations.Library
if (query.EnableTotalRecordCount)
{
- return ItemRepository.GetItems(query);
+ return _itemRepository.GetItems(query);
}
- var list = ItemRepository.GetItemList(query);
+ var list = _itemRepository.GetItemList(query);
return new QueryResult<BaseItem>
{
@@ -1509,7 +1480,7 @@ namespace Emby.Server.Implementations.Library
string.IsNullOrEmpty(query.SeriesPresentationUniqueKey) &&
query.ItemIds.Length == 0)
{
- var userViews = _userviewManager().GetUserViews(new UserViewQuery
+ var userViews = UserViewManager.GetUserViews(new UserViewQuery
{
UserId = user.Id,
IncludeHidden = true,
@@ -1809,7 +1780,7 @@ namespace Emby.Server.Implementations.Library
// Don't iterate multiple times
var itemsList = items.ToList();
- ItemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(itemsList, cancellationToken);
foreach (var item in itemsList)
{
@@ -1846,7 +1817,7 @@ namespace Emby.Server.Implementations.Library
public void UpdateImages(BaseItem item)
{
- ItemRepository.SaveImages(item);
+ _itemRepository.SaveImages(item);
RegisterItem(item);
}
@@ -1863,7 +1834,7 @@ namespace Emby.Server.Implementations.Library
{
if (item.IsFileProtocol)
{
- _providerManagerFactory().SaveMetadata(item, updateReason);
+ ProviderManager.SaveMetadata(item, updateReason);
}
item.DateLastSaved = DateTime.UtcNow;
@@ -1871,7 +1842,7 @@ namespace Emby.Server.Implementations.Library
RegisterItem(item);
}
- ItemRepository.SaveItems(itemsList, cancellationToken);
+ _itemRepository.SaveItems(itemsList, cancellationToken);
if (ItemUpdated != null)
{
@@ -1947,7 +1918,7 @@ namespace Emby.Server.Implementations.Library
/// <returns>BaseItem.</returns>
public BaseItem RetrieveItem(Guid id)
{
- return ItemRepository.RetrieveItem(id);
+ return _itemRepository.RetrieveItem(id);
}
public List<Folder> GetCollectionFolders(BaseItem item)
@@ -2066,7 +2037,7 @@ namespace Emby.Server.Implementations.Library
private string GetContentTypeOverride(string path, bool inherit)
{
- var nameValuePair = ConfigurationManager.Configuration.ContentTypes
+ var nameValuePair = _configurationManager.Configuration.ContentTypes
.FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
|| (inherit && !string.IsNullOrEmpty(i.Name)
&& _fileSystem.ContainsSubPath(i.Name, path)));
@@ -2115,7 +2086,7 @@ namespace Emby.Server.Implementations.Library
string sortName)
{
var path = Path.Combine(
- ConfigurationManager.ApplicationPaths.InternalMetadataPath,
+ _configurationManager.ApplicationPaths.InternalMetadataPath,
"views",
_fileSystem.GetValidFilename(viewType));
@@ -2147,7 +2118,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
item.UpdateToRepository(ItemUpdateType.MetadataImport, CancellationToken.None);
- _providerManagerFactory().QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
+ ProviderManager.QueueRefresh(item.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.Normal);
}
return item;
@@ -2165,7 +2136,7 @@ namespace Emby.Server.Implementations.Library
var id = GetNewItemId(idValues, typeof(UserView));
- var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
+ var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
var item = GetItemById(id) as UserView;
@@ -2202,7 +2173,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
- _providerManagerFactory().QueueRefresh(
+ ProviderManager.QueueRefresh(
item.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -2269,7 +2240,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
- _providerManagerFactory().QueueRefresh(
+ ProviderManager.QueueRefresh(
item.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -2303,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
var id = GetNewItemId(idValues, typeof(UserView));
- var path = Path.Combine(ConfigurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
+ var path = Path.Combine(_configurationManager.ApplicationPaths.InternalMetadataPath, "views", id.ToString("N", CultureInfo.InvariantCulture));
var item = GetItemById(id) as UserView;
@@ -2346,7 +2317,7 @@ namespace Emby.Server.Implementations.Library
if (refresh)
{
- _providerManagerFactory().QueueRefresh(
+ ProviderManager.QueueRefresh(
item.Id,
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
@@ -2364,7 +2335,7 @@ namespace Emby.Server.Implementations.Library
string videoPath,
string[] files)
{
- new SubtitleResolver(BaseItem.LocalizationManager, _fileSystem).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
+ new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files);
}
/// <inheritdoc />
@@ -2675,8 +2646,8 @@ namespace Emby.Server.Implementations.Library
}
}
- var metadataPath = ConfigurationManager.Configuration.MetadataPath;
- var metadataNetworkPath = ConfigurationManager.Configuration.MetadataNetworkPath;
+ var metadataPath = _configurationManager.Configuration.MetadataPath;
+ var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
{
@@ -2687,7 +2658,7 @@ namespace Emby.Server.Implementations.Library
}
}
- foreach (var map in ConfigurationManager.Configuration.PathSubstitutions)
+ foreach (var map in _configurationManager.Configuration.PathSubstitutions)
{
if (!string.IsNullOrWhiteSpace(map.From))
{
@@ -2756,7 +2727,7 @@ namespace Emby.Server.Implementations.Library
public List<PersonInfo> GetPeople(InternalPeopleQuery query)
{
- return ItemRepository.GetPeople(query);
+ return _itemRepository.GetPeople(query);
}
public List<PersonInfo> GetPeople(BaseItem item)
@@ -2779,7 +2750,7 @@ namespace Emby.Server.Implementations.Library
public List<Person> GetPeopleItems(InternalPeopleQuery query)
{
- return ItemRepository.GetPeopleNames(query).Select(i =>
+ return _itemRepository.GetPeopleNames(query).Select(i =>
{
try
{
@@ -2796,7 +2767,7 @@ namespace Emby.Server.Implementations.Library
public List<string> GetPeopleNames(InternalPeopleQuery query)
{
- return ItemRepository.GetPeopleNames(query);
+ return _itemRepository.GetPeopleNames(query);
}
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
@@ -2806,7 +2777,7 @@ namespace Emby.Server.Implementations.Library
return;
}
- ItemRepository.UpdatePeople(item.Id, people);
+ _itemRepository.UpdatePeople(item.Id, people);
}
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
@@ -2817,7 +2788,7 @@ namespace Emby.Server.Implementations.Library
{
_logger.LogDebug("ConvertImageToLocal item {0} - image url: {1}", item.Id, url);
- await _providerManagerFactory().SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
+ await ProviderManager.SaveImage(item, url, image.Type, imageIndex, CancellationToken.None).ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
@@ -2850,7 +2821,7 @@ namespace Emby.Server.Implementations.Library
name = _fileSystem.GetValidFilename(name);
- var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, name);
while (Directory.Exists(virtualFolderPath))
@@ -2869,7 +2840,7 @@ namespace Emby.Server.Implementations.Library
}
}
- _libraryMonitorFactory().Stop();
+ LibraryMonitor.Stop();
try
{
@@ -2904,7 +2875,7 @@ namespace Emby.Server.Implementations.Library
{
// Need to add a delay here or directory watchers may still pick up the changes
await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitorFactory().Start();
+ LibraryMonitor.Start();
}
}
}
@@ -2964,7 +2935,7 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The network path does not exist.");
}
- var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var shortcutFilename = Path.GetFileNameWithoutExtension(path);
@@ -3007,7 +2978,7 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The network path does not exist.");
}
- var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
var libraryOptions = CollectionFolder.GetLibraryOptions(virtualFolderPath);
@@ -3060,7 +3031,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(name));
}
- var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var path = Path.Combine(rootFolderPath, name);
@@ -3069,7 +3040,7 @@ namespace Emby.Server.Implementations.Library
throw new FileNotFoundException("The media folder does not exist");
}
- _libraryMonitorFactory().Stop();
+ LibraryMonitor.Stop();
try
{
@@ -3089,7 +3060,7 @@ namespace Emby.Server.Implementations.Library
{
// Need to add a delay here or directory watchers may still pick up the changes
await Task.Delay(1000).ConfigureAwait(false);
- _libraryMonitorFactory().Start();
+ LibraryMonitor.Start();
}
}
}
@@ -3103,7 +3074,7 @@ namespace Emby.Server.Implementations.Library
var removeList = new List<NameValuePair>();
- foreach (var contentType in ConfigurationManager.Configuration.ContentTypes)
+ foreach (var contentType in _configurationManager.Configuration.ContentTypes)
{
if (string.IsNullOrWhiteSpace(contentType.Name))
{
@@ -3118,11 +3089,11 @@ namespace Emby.Server.Implementations.Library
if (removeList.Count > 0)
{
- ConfigurationManager.Configuration.ContentTypes = ConfigurationManager.Configuration.ContentTypes
+ _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
.Except(removeList)
- .ToArray();
+ .ToArray();
- ConfigurationManager.SaveConfiguration();
+ _configurationManager.SaveConfiguration();
}
}
@@ -3133,7 +3104,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentNullException(nameof(mediaPath));
}
- var rootFolderPath = ConfigurationManager.ApplicationPaths.DefaultUserViewsPath;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
if (!Directory.Exists(virtualFolderPath))
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index 70d5bd9f4..01fe98f3a 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -33,13 +33,13 @@ namespace Emby.Server.Implementations.Library
private readonly ILibraryManager _libraryManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly IFileSystem _fileSystem;
-
- private IMediaSourceProvider[] _providers;
private readonly ILogger _logger;
private readonly IUserDataManager _userDataManager;
- private readonly Func<IMediaEncoder> _mediaEncoder;
- private ILocalizationManager _localizationManager;
- private IApplicationPaths _appPaths;
+ private readonly IMediaEncoder _mediaEncoder;
+ private readonly ILocalizationManager _localizationManager;
+ private readonly IApplicationPaths _appPaths;
+
+ private IMediaSourceProvider[] _providers;
public MediaSourceManager(
IItemRepository itemRepo,
@@ -47,16 +47,16 @@ namespace Emby.Server.Implementations.Library
ILocalizationManager localizationManager,
IUserManager userManager,
ILibraryManager libraryManager,
- ILoggerFactory loggerFactory,
+ ILogger<MediaSourceManager> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IUserDataManager userDataManager,
- Func<IMediaEncoder> mediaEncoder)
+ IMediaEncoder mediaEncoder)
{
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
- _logger = loggerFactory.CreateLogger(nameof(MediaSourceManager));
+ _logger = logger;
_jsonSerializer = jsonSerializer;
_fileSystem = fileSystem;
_userDataManager = userDataManager;
@@ -496,7 +496,7 @@ namespace Emby.Server.Implementations.Library
// hack - these two values were taken from LiveTVMediaSourceProvider
string cacheKey = request.OpenToken;
- await new LiveStreamHelper(_mediaEncoder(), _logger, _jsonSerializer, _appPaths)
+ await new LiveStreamHelper(_mediaEncoder, _logger, _jsonSerializer, _appPaths)
.AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
.ConfigureAwait(false);
}
@@ -621,7 +621,7 @@ namespace Emby.Server.Implementations.Library
if (liveStreamInfo is IDirectStreamProvider)
{
- var info = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest
+ var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
{
MediaSource = mediaSource,
ExtractChapters = false,
@@ -674,7 +674,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
}
- mediaInfo = await _mediaEncoder().GetMediaInfo(new MediaInfoRequest
+ mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
index 6b9f4d052..e27145a1d 100644
--- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs
+++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs
@@ -35,7 +35,8 @@ namespace Emby.Server.Implementations.Library
return null;
}
- public static int? GetDefaultSubtitleStreamIndex(List<MediaStream> streams,
+ public static int? GetDefaultSubtitleStreamIndex(
+ List<MediaStream> streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
@@ -115,7 +116,8 @@ namespace Emby.Server.Implementations.Library
.ThenBy(i => i.Index);
}
- public static void SetSubtitleStreamScores(List<MediaStream> streams,
+ public static void SetSubtitleStreamScores(
+ List<MediaStream> streams,
string[] preferredLanguages,
SubtitlePlaybackMode mode,
string audioTrackLanguage)
diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 4fdf73b77..06ff3e611 100644
--- a/Emby.Server.Implementations/Library/PathExtensions.cs
+++ b/Emby.Server.Implementations/Library/PathExtensions.cs
@@ -1,3 +1,5 @@
+#nullable enable
+
using System;
using System.Text.RegularExpressions;
@@ -12,24 +14,24 @@ namespace Emby.Server.Implementations.Library
/// Gets the attribute value.
/// </summary>
/// <param name="str">The STR.</param>
- /// <param name="attrib">The attrib.</param>
+ /// <param name="attribute">The attrib.</param>
/// <returns>System.String.</returns>
- /// <exception cref="ArgumentNullException">attrib</exception>
- public static string GetAttributeValue(this string str, string attrib)
+ /// <exception cref="ArgumentException"><paramref name="str" /> or <paramref name="attribute" /> is empty.</exception>
+ public static string? GetAttributeValue(this string str, string attribute)
{
- if (string.IsNullOrEmpty(str))
+ if (str.Length == 0)
{
- throw new ArgumentNullException(nameof(str));
+ throw new ArgumentException("String can't be empty.", nameof(str));
}
- if (string.IsNullOrEmpty(attrib))
+ if (attribute.Length == 0)
{
- throw new ArgumentNullException(nameof(attrib));
+ throw new ArgumentException("String can't be empty.", nameof(attribute));
}
- string srch = "[" + attrib + "=";
+ string srch = "[" + attribute + "=";
int start = str.IndexOf(srch, StringComparison.OrdinalIgnoreCase);
- if (start > -1)
+ if (start != -1)
{
start += srch.Length;
int end = str.IndexOf(']', start);
@@ -37,9 +39,9 @@ namespace Emby.Server.Implementations.Library
}
// for imdbid we also accept pattern matching
- if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase))
{
- var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase);
+ var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase);
return m.Success ? m.Value : null;
}
diff --git a/Emby.Server.Implementations/Library/ResolverHelper.cs b/Emby.Server.Implementations/Library/ResolverHelper.cs
index 34dcbbe28..7ca15b4e5 100644
--- a/Emby.Server.Implementations/Library/ResolverHelper.cs
+++ b/Emby.Server.Implementations/Library/ResolverHelper.cs
@@ -118,10 +118,12 @@ namespace Emby.Server.Implementations.Library
{
throw new ArgumentNullException(nameof(fileSystem));
}
+
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
+
if (args == null)
{
throw new ArgumentNullException(nameof(args));
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
index 85b1b6e32..6c9ba7c27 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
@@ -16,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicAlbumResolver : ItemResolver<MusicAlbum>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
@@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <param name="logger">The logger.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
- public MusicAlbumResolver(ILogger logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+ public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
{
_logger = logger;
_fileSystem = fileSystem;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
index 681db4896..5f5cd0e92 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
@@ -15,7 +15,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// </summary>
public class MusicArtistResolver : ItemResolver<MusicArtist>
{
- private readonly ILogger _logger;
+ private readonly ILogger<MusicAlbumResolver> _logger;
private readonly IFileSystem _fileSystem;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
@@ -23,12 +23,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
/// <summary>
/// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
/// </summary>
- /// <param name="logger">The logger.</param>
+ /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
/// <param name="fileSystem">The file system.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The configuration manager.</param>
public MusicArtistResolver(
- ILogger<MusicArtistResolver> logger,
+ ILogger<MusicAlbumResolver> logger,
IFileSystem fileSystem,
ILibraryManager libraryManager,
IServerConfigurationManager config)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 0b93ebeb8..503de0b4e 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -11,7 +11,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{
public class BookResolver : MediaBrowser.Controller.Resolvers.ItemResolver<Book>
{
- private readonly string[] _validExtensions = { ".pdf", ".epub", ".mobi", ".cbr", ".cbz", ".azw3" };
+ private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".opf", ".pdf" };
protected override Book Resolve(ItemResolveArgs args)
{
diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs
index 11d6c737a..59a77607d 100644
--- a/Emby.Server.Implementations/Library/SearchEngine.cs
+++ b/Emby.Server.Implementations/Library/SearchEngine.cs
@@ -17,16 +17,15 @@ namespace Emby.Server.Implementations.Library
{
public class SearchEngine : ISearchEngine
{
+ private readonly ILogger _logger;
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
- private readonly ILogger _logger;
- public SearchEngine(ILoggerFactory loggerFactory, ILibraryManager libraryManager, IUserManager userManager)
+ public SearchEngine(ILogger<SearchEngine> logger, ILibraryManager libraryManager, IUserManager userManager)
{
+ _logger = logger;
_libraryManager = libraryManager;
_userManager = userManager;
-
- _logger = loggerFactory.CreateLogger("SearchEngine");
}
public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index 071681b08..a9772a078 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -28,25 +28,24 @@ namespace Emby.Server.Implementations.Library
private readonly ILogger _logger;
private readonly IServerConfigurationManager _config;
-
- private Func<IUserManager> _userManager;
-
- public UserDataManager(ILoggerFactory loggerFactory, IServerConfigurationManager config, Func<IUserManager> userManager)
+ private readonly IUserManager _userManager;
+ private readonly IUserDataRepository _repository;
+
+ public UserDataManager(
+ ILogger<UserDataManager> logger,
+ IServerConfigurationManager config,
+ IUserManager userManager,
+ IUserDataRepository repository)
{
+ _logger = logger;
_config = config;
- _logger = loggerFactory.CreateLogger(GetType().Name);
_userManager = userManager;
+ _repository = repository;
}
- /// <summary>
- /// Gets or sets the repository.
- /// </summary>
- /// <value>The repository.</value>
- public IUserDataRepository Repository { get; set; }
-
public void SaveUserData(Guid userId, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken)
{
- var user = _userManager().GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
SaveUserData(user, item, userData, reason, cancellationToken);
}
@@ -71,7 +70,7 @@ namespace Emby.Server.Implementations.Library
foreach (var key in keys)
{
- Repository.SaveUserData(userId, key, userData, cancellationToken);
+ _repository.SaveUserData(userId, key, userData, cancellationToken);
}
var cacheKey = GetCacheKey(userId, item.Id);
@@ -96,9 +95,9 @@ namespace Emby.Server.Implementations.Library
/// <returns></returns>
public void SaveAllUserData(Guid userId, UserItemData[] userData, CancellationToken cancellationToken)
{
- var user = _userManager().GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
- Repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
+ _repository.SaveAllUserData(user.InternalId, userData, cancellationToken);
}
/// <summary>
@@ -108,14 +107,14 @@ namespace Emby.Server.Implementations.Library
/// <returns></returns>
public List<UserItemData> GetAllUserData(Guid userId)
{
- var user = _userManager().GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
- return Repository.GetAllUserData(user.InternalId);
+ return _repository.GetAllUserData(user.InternalId);
}
public UserItemData GetUserData(Guid userId, Guid itemId, List<string> keys)
{
- var user = _userManager().GetUserById(userId);
+ var user = _userManager.GetUserById(userId);
return GetUserData(user, itemId, keys);
}
@@ -131,7 +130,7 @@ namespace Emby.Server.Implementations.Library
private UserItemData GetUserDataInternal(long internalUserId, List<string> keys)
{
- var userData = Repository.GetUserData(internalUserId, keys);
+ var userData = _repository.GetUserData(internalUserId, keys);
if (userData != null)
{
diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs
index 7b17cc913..d63bc6bda 100644
--- a/Emby.Server.Implementations/Library/UserManager.cs
+++ b/Emby.Server.Implementations/Library/UserManager.cs
@@ -20,6 +20,7 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Providers;
@@ -44,22 +45,14 @@ namespace Emby.Server.Implementations.Library
{
private readonly object _policySyncLock = new object();
private readonly object _configSyncLock = new object();
- /// <summary>
- /// The logger.
- /// </summary>
- private readonly ILogger _logger;
- /// <summary>
- /// Gets the active user repository.
- /// </summary>
- /// <value>The user repository.</value>
+ private readonly ILogger _logger;
private readonly IUserRepository _userRepository;
private readonly IXmlSerializer _xmlSerializer;
private readonly IJsonSerializer _jsonSerializer;
private readonly INetworkManager _networkManager;
-
- private readonly Func<IImageProcessor> _imageProcessorFactory;
- private readonly Func<IDtoService> _dtoServiceFactory;
+ private readonly IImageProcessor _imageProcessor;
+ private readonly Lazy<IDtoService> _dtoServiceFactory;
private readonly IServerApplicationHost _appHost;
private readonly IFileSystem _fileSystem;
private readonly ICryptoProvider _cryptoProvider;
@@ -74,13 +67,15 @@ namespace Emby.Server.Implementations.Library
private IPasswordResetProvider[] _passwordResetProviders;
private DefaultPasswordResetProvider _defaultPasswordResetProvider;
+ private IDtoService DtoService => _dtoServiceFactory.Value;
+
public UserManager(
ILogger<UserManager> logger,
IUserRepository userRepository,
IXmlSerializer xmlSerializer,
INetworkManager networkManager,
- Func<IImageProcessor> imageProcessorFactory,
- Func<IDtoService> dtoServiceFactory,
+ IImageProcessor imageProcessor,
+ Lazy<IDtoService> dtoServiceFactory,
IServerApplicationHost appHost,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
@@ -90,7 +85,7 @@ namespace Emby.Server.Implementations.Library
_userRepository = userRepository;
_xmlSerializer = xmlSerializer;
_networkManager = networkManager;
- _imageProcessorFactory = imageProcessorFactory;
+ _imageProcessor = imageProcessor;
_dtoServiceFactory = dtoServiceFactory;
_appHost = appHost;
_jsonSerializer = jsonSerializer;
@@ -264,6 +259,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.IsNullOrWhiteSpace(username))
{
+ _logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
throw new ArgumentNullException(nameof(username));
}
@@ -319,26 +315,26 @@ namespace Emby.Server.Implementations.Library
if (user == null)
{
+ _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", username, remoteEndPoint);
throw new AuthenticationException("Invalid username or password entered.");
}
if (user.Policy.IsDisabled)
{
- throw new AuthenticationException(
- string.Format(
- CultureInfo.InvariantCulture,
- "The {0} account is currently disabled. Please consult with your administrator.",
- user.Name));
+ _logger.LogInformation("Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).", username, remoteEndPoint);
+ throw new SecurityException($"The {user.Name} account is currently disabled. Please consult with your administrator.");
}
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(remoteEndPoint))
{
- throw new AuthenticationException("Forbidden.");
+ _logger.LogInformation("Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).", username, remoteEndPoint);
+ throw new SecurityException("Forbidden.");
}
if (!user.IsParentalScheduleAllowed())
{
- throw new AuthenticationException("User is not allowed access at this time.");
+ _logger.LogInformation("Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).", username, remoteEndPoint);
+ throw new SecurityException("User is not allowed access at this time.");
}
// Update LastActivityDate and LastLoginDate, then save
@@ -351,14 +347,14 @@ namespace Emby.Server.Implementations.Library
}
ResetInvalidLoginAttemptCount(user);
+ _logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Name);
}
else
{
IncrementInvalidLoginAttemptCount(user);
+ _logger.LogInformation("Authentication request for {UserName} has been denied (IP: {IP}).", user.Name, remoteEndPoint);
}
- _logger.LogInformation("Authentication request for {0} {1}.", user.Name, success ? "has succeeded" : "has been denied");
-
return success ? user : null;
}
@@ -600,7 +596,7 @@ namespace Emby.Server.Implementations.Library
try
{
- _dtoServiceFactory().AttachPrimaryImageAspectRatio(dto, user);
+ DtoService.AttachPrimaryImageAspectRatio(dto, user);
}
catch (Exception ex)
{
@@ -625,7 +621,7 @@ namespace Emby.Server.Implementations.Library
{
try
{
- return _imageProcessorFactory().GetImageCacheTag(item, image);
+ return _imageProcessor.GetImageCacheTag(item, image);
}
catch (Exception ex)
{
diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
index 900f12062..3efe1ee25 100644
--- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
+++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
@@ -1059,7 +1059,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{
var stream = new MediaSourceInfo
{
- EncoderPath = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+ EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
EncoderProtocol = MediaProtocol.Http,
Path = info.Path,
Protocol = MediaProtocol.File,
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
index 6e903a18e..a59c1090e 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
@@ -22,9 +22,12 @@ namespace Emby.Server.Implementations.LiveTv
{
public class LiveTvDtoService
{
+ private const string InternalVersionNumber = "4";
+
+ private const string ServiceName = "Emby";
+
private readonly ILogger _logger;
private readonly IImageProcessor _imageProcessor;
-
private readonly IDtoService _dtoService;
private readonly IApplicationHost _appHost;
private readonly ILibraryManager _libraryManager;
@@ -32,13 +35,13 @@ namespace Emby.Server.Implementations.LiveTv
public LiveTvDtoService(
IDtoService dtoService,
IImageProcessor imageProcessor,
- ILoggerFactory loggerFactory,
+ ILogger<LiveTvDtoService> logger,
IApplicationHost appHost,
ILibraryManager libraryManager)
{
_dtoService = dtoService;
_imageProcessor = imageProcessor;
- _logger = loggerFactory.CreateLogger(nameof(LiveTvDtoService));
+ _logger = logger;
_appHost = appHost;
_libraryManager = libraryManager;
}
@@ -161,7 +164,6 @@ namespace Emby.Server.Implementations.LiveTv
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (librarySeries != null)
@@ -179,6 +181,7 @@ namespace Emby.Server.Implementations.LiveTv
_logger.LogError(ex, "Error");
}
}
+
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
if (image != null)
{
@@ -199,13 +202,12 @@ namespace Emby.Server.Implementations.LiveTv
var 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 },
DtoOptions = new DtoOptions(false),
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
}).FirstOrDefault();
if (program != null)
@@ -232,9 +234,10 @@ namespace Emby.Server.Implementations.LiveTv
try
{
dto.ParentBackdropImageTags = new string[]
- {
+ {
_imageProcessor.GetImageCacheTag(program, image)
- };
+ };
+
dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture);
}
catch (Exception ex)
@@ -255,7 +258,6 @@ namespace Emby.Server.Implementations.LiveTv
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Thumb },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (librarySeries != null)
@@ -273,6 +275,7 @@ namespace Emby.Server.Implementations.LiveTv
_logger.LogError(ex, "Error");
}
}
+
image = librarySeries.GetImageInfo(ImageType.Backdrop, 0);
if (image != null)
{
@@ -298,7 +301,6 @@ namespace Emby.Server.Implementations.LiveTv
Limit = 1,
ImageTypes = new ImageType[] { ImageType.Primary },
DtoOptions = new DtoOptions(false)
-
}).FirstOrDefault();
if (program == null)
@@ -311,7 +313,6 @@ namespace Emby.Server.Implementations.LiveTv
ImageTypes = new ImageType[] { ImageType.Primary },
DtoOptions = new DtoOptions(false),
Name = string.IsNullOrEmpty(programSeriesId) ? seriesName : null
-
}).FirstOrDefault();
}
@@ -396,8 +397,6 @@ namespace Emby.Server.Implementations.LiveTv
return null;
}
- private const string InternalVersionNumber = "4";
-
public Guid GetInternalChannelId(string serviceName, string externalId)
{
var name = serviceName + externalId + InternalVersionNumber;
@@ -405,7 +404,6 @@ namespace Emby.Server.Implementations.LiveTv
return _libraryManager.GetNewItemId(name.ToLowerInvariant(), typeof(LiveTvChannel));
}
- private const string ServiceName = "Emby";
public string GetInternalTimerId(string externalId)
{
var name = ServiceName + externalId + InternalVersionNumber;
diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
index b64fe8634..1b10f2d27 100644
--- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
+++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs
@@ -41,33 +41,32 @@ namespace Emby.Server.Implementations.LiveTv
/// </summary>
public class LiveTvManager : ILiveTvManager, IDisposable
{
+ private const string ExternalServiceTag = "ExternalServiceId";
+
+ private const string EtagKey = "ProgramEtag";
+
private readonly IServerConfigurationManager _config;
private readonly ILogger _logger;
private readonly IItemRepository _itemRepo;
private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
private readonly IUserDataManager _userDataManager;
private readonly ILibraryManager _libraryManager;
private readonly ITaskManager _taskManager;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly Func<IChannelManager> _channelManager;
-
- private readonly IDtoService _dtoService;
private readonly ILocalizationManager _localization;
-
+ private readonly IJsonSerializer _jsonSerializer;
+ private readonly IFileSystem _fileSystem;
+ private readonly IChannelManager _channelManager;
private readonly LiveTvDtoService _tvDtoService;
private ILiveTvService[] _services = Array.Empty<ILiveTvService>();
-
private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>();
private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>();
- private readonly IFileSystem _fileSystem;
public LiveTvManager(
- IServerApplicationHost appHost,
IServerConfigurationManager config,
- ILoggerFactory loggerFactory,
+ ILogger<LiveTvManager> logger,
IItemRepository itemRepo,
- IImageProcessor imageProcessor,
IUserDataManager userDataManager,
IDtoService dtoService,
IUserManager userManager,
@@ -76,10 +75,11 @@ namespace Emby.Server.Implementations.LiveTv
ILocalizationManager localization,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
- Func<IChannelManager> channelManager)
+ IChannelManager channelManager,
+ LiveTvDtoService liveTvDtoService)
{
_config = config;
- _logger = loggerFactory.CreateLogger(nameof(LiveTvManager));
+ _logger = logger;
_itemRepo = itemRepo;
_userManager = userManager;
_libraryManager = libraryManager;
@@ -90,8 +90,7 @@ namespace Emby.Server.Implementations.LiveTv
_dtoService = dtoService;
_userDataManager = userDataManager;
_channelManager = channelManager;
-
- _tvDtoService = new LiveTvDtoService(dtoService, imageProcessor, loggerFactory, appHost, _libraryManager);
+ _tvDtoService = liveTvDtoService;
}
public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled;
@@ -178,7 +177,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Name = i.Name,
Id = i.Type
-
}).ToList();
}
@@ -261,6 +259,7 @@ namespace Emby.Server.Implementations.LiveTv
var endTime = DateTime.UtcNow;
_logger.LogInformation("Live stream opened after {0}ms", (endTime - startTime).TotalMilliseconds);
}
+
info.RequiresClosing = true;
var idPrefix = service.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture) + "_";
@@ -362,30 +361,37 @@ namespace Emby.Server.Implementations.LiveTv
{
stream.BitRate = null;
}
+
if (stream.Channels.HasValue && stream.Channels <= 0)
{
stream.Channels = null;
}
+
if (stream.AverageFrameRate.HasValue && stream.AverageFrameRate <= 0)
{
stream.AverageFrameRate = null;
}
+
if (stream.RealFrameRate.HasValue && stream.RealFrameRate <= 0)
{
stream.RealFrameRate = null;
}
+
if (stream.Width.HasValue && stream.Width <= 0)
{
stream.Width = null;
}
+
if (stream.Height.HasValue && stream.Height <= 0)
{
stream.Height = null;
}
+
if (stream.SampleRate.HasValue && stream.SampleRate <= 0)
{
stream.SampleRate = null;
}
+
if (stream.Level.HasValue && stream.Level <= 0)
{
stream.Level = null;
@@ -427,7 +433,6 @@ namespace Emby.Server.Implementations.LiveTv
}
}
- private const string ExternalServiceTag = "ExternalServiceId";
private LiveTvChannel GetChannel(ChannelInfo channelInfo, string serviceName, BaseItem parentFolder, CancellationToken cancellationToken)
{
var parentFolderId = parentFolder.Id;
@@ -456,6 +461,7 @@ namespace Emby.Server.Implementations.LiveTv
{
isNew = true;
}
+
item.Tags = channelInfo.Tags;
}
@@ -463,6 +469,7 @@ namespace Emby.Server.Implementations.LiveTv
{
isNew = true;
}
+
item.ParentId = parentFolderId;
item.ChannelType = channelInfo.ChannelType;
@@ -472,24 +479,28 @@ namespace Emby.Server.Implementations.LiveTv
{
forceUpdate = true;
}
+
item.SetProviderId(ExternalServiceTag, serviceName);
if (!string.Equals(channelInfo.Id, item.ExternalId, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.ExternalId = channelInfo.Id;
if (!string.Equals(channelInfo.Number, item.Number, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.Number = channelInfo.Number;
if (!string.Equals(channelInfo.Name, item.Name, StringComparison.Ordinal))
{
forceUpdate = true;
}
+
item.Name = channelInfo.Name;
if (!item.HasImage(ImageType.Primary))
@@ -518,8 +529,6 @@ namespace Emby.Server.Implementations.LiveTv
return item;
}
- private const string EtagKey = "ProgramEtag";
-
private Tuple<LiveTvProgram, bool, bool> GetProgram(ProgramInfo info, Dictionary<Guid, LiveTvProgram> allExistingPrograms, LiveTvChannel channel, ChannelType channelType, string serviceName, CancellationToken cancellationToken)
{
var id = _tvDtoService.GetInternalProgramId(info.Id);
@@ -2482,7 +2491,7 @@ namespace Emby.Server.Implementations.LiveTv
.OrderBy(i => i.SortName)
.ToList();
- folders.AddRange(_channelManager().GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+ folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
{
UserId = user.Id,
IsRecordingsFolder = true,
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 03ee5bfb6..82b1f3cf1 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
//OpenedMediaSource.Path = tempFile;
//OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
//OpenedMediaSource.SupportsDirectPlay = false;
//OpenedMediaSource.SupportsDirectStream = true;
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
index f5dda79db..f7c9c736e 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
@@ -35,7 +35,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
public M3UTunerHost(
IServerConfigurationManager config,
IMediaSourceManager mediaSourceManager,
- ILogger logger,
+ ILogger<M3UTunerHost> logger,
IJsonSerializer jsonSerializer,
IFileSystem fileSystem,
IHttpClient httpClient,
@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list);
}
- private static readonly string[] _disallowedSharedStreamExtensions = new string[]
+ private static readonly string[] _disallowedSharedStreamExtensions =
{
".mkv",
".mp4",
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index d63588bbd..322fbbbaa 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Threading;
@@ -106,7 +107,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
//OpenedMediaSource.Path = tempFile;
//OpenedMediaSource.ReadAtNativeFramerate = true;
- MediaSource.Path = _appHost.GetLocalApiUrl("127.0.0.1") + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+ MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
MediaSource.Protocol = MediaProtocol.Http;
//OpenedMediaSource.Path = TempFilePath;
@@ -118,6 +119,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
//OpenedMediaSource.SupportsDirectStream = true;
//OpenedMediaSource.SupportsTranscoding = true;
await taskCompletionSource.Task.ConfigureAwait(false);
+ if (taskCompletionSource.Task.Exception != null)
+ {
+ // Error happened while opening the stream so raise the exception again to inform the caller
+ throw taskCompletionSource.Task.Exception;
+ }
+
+ if (!taskCompletionSource.Task.Result)
+ {
+ Logger.LogWarning("Zero bytes copied from stream {0} to {1} but no exception raised", GetType().Name, TempFilePath);
+ throw new EndOfStreamException(String.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
+ }
}
private Task StartStreaming(HttpResponseInfo response, TaskCompletionSource<bool> openTaskCompletionSource, CancellationToken cancellationToken)
@@ -139,14 +151,19 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
cancellationToken).ConfigureAwait(false);
}
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
+ Logger.LogInformation("Copying of {0} to {1} was canceled", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error copying live stream.");
+ Logger.LogError(ex, "Error copying live stream {0} to {1}.", GetType().Name, TempFilePath);
+ openTaskCompletionSource.TrySetException(ex);
}
+ openTaskCompletionSource.TrySetResult(false);
+
EnableStreamSharing = false;
await DeleteTempFiles(new List<string> { TempFilePath }).ConfigureAwait(false);
});
diff --git a/Emby.Server.Implementations/Localization/Core/af.json b/Emby.Server.Implementations/Localization/Core/af.json
index 1363eaf85..20447347b 100644
--- a/Emby.Server.Implementations/Localization/Core/af.json
+++ b/Emby.Server.Implementations/Localization/Core/af.json
@@ -4,7 +4,7 @@
"Folders": "Fouers",
"Favorites": "Gunstelinge",
"HeaderFavoriteShows": "Gunsteling Vertonings",
- "ValueSpecialEpisodeName": "Spesiaal - {0}",
+ "ValueSpecialEpisodeName": "Spesiale - {0}",
"HeaderAlbumArtists": "Album Kunstenaars",
"Books": "Boeke",
"HeaderNextUp": "Volgende",
diff --git a/Emby.Server.Implementations/Localization/Core/ar.json b/Emby.Server.Implementations/Localization/Core/ar.json
index f313039a6..d68928fce 100644
--- a/Emby.Server.Implementations/Localization/Core/ar.json
+++ b/Emby.Server.Implementations/Localization/Core/ar.json
@@ -9,7 +9,7 @@
"Channels": "القنوات",
"ChapterNameValue": "الفصل {0}",
"Collections": "مجموعات",
- "DeviceOfflineWithName": "قُطِع الاتصال بـ{0}",
+ "DeviceOfflineWithName": "قُطِع الاتصال ب{0}",
"DeviceOnlineWithName": "{0} متصل",
"FailedLoginAttemptWithUserName": "عملية تسجيل الدخول فشلت من {0}",
"Favorites": "المفضلة",
diff --git a/Emby.Server.Implementations/Localization/Core/bn.json b/Emby.Server.Implementations/Localization/Core/bn.json
index ef7792356..4949b10e6 100644
--- a/Emby.Server.Implementations/Localization/Core/bn.json
+++ b/Emby.Server.Implementations/Localization/Core/bn.json
@@ -91,5 +91,7 @@
"HeaderNextUp": "এরপরে আসছে",
"HeaderLiveTV": "লাইভ টিভি",
"HeaderFavoriteSongs": "প্রিয় গানগুলো",
- "HeaderFavoriteShows": "প্রিয় শোগুলো"
+ "HeaderFavoriteShows": "প্রিয় শোগুলো",
+ "TasksLibraryCategory": "গ্রন্থাগার",
+ "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json
index 2d8299367..7464ac1c0 100644
--- a/Emby.Server.Implementations/Localization/Core/ca.json
+++ b/Emby.Server.Implementations/Localization/Core/ca.json
@@ -3,19 +3,19 @@
"AppDeviceValues": "Aplicació: {0}, Dispositiu: {1}",
"Application": "Aplicació",
"Artists": "Artistes",
- "AuthenticationSucceededWithUserName": "{0} s'ha autentificat correctament",
+ "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
"Books": "Llibres",
- "CameraImageUploadedFrom": "Una nova imatge de la càmera ha sigut pujada des de {0}",
+ "CameraImageUploadedFrom": "Una nova imatge de la càmera ha estat pujada des de {0}",
"Channels": "Canals",
- "ChapterNameValue": "Episodi {0}",
+ "ChapterNameValue": "Capítol {0}",
"Collections": "Col·leccions",
"DeviceOfflineWithName": "{0} s'ha desconnectat",
"DeviceOnlineWithName": "{0} està connectat",
"FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
"Favorites": "Preferits",
- "Folders": "Directoris",
+ "Folders": "Carpetes",
"Genres": "Gèneres",
- "HeaderAlbumArtists": "Artistes dels Àlbums",
+ "HeaderAlbumArtists": "Artistes del Àlbum",
"HeaderCameraUploads": "Pujades de Càmera",
"HeaderContinueWatching": "Continua Veient",
"HeaderFavoriteAlbums": "Àlbums Preferits",
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 992bb9df3..464ca28ca 100644
--- a/Emby.Server.Implementations/Localization/Core/cs.json
+++ b/Emby.Server.Implementations/Localization/Core/cs.json
@@ -23,7 +23,7 @@
"HeaderFavoriteEpisodes": "Oblíbené epizody",
"HeaderFavoriteShows": "Oblíbené seriály",
"HeaderFavoriteSongs": "Oblíbená hudba",
- "HeaderLiveTV": "Živá TV",
+ "HeaderLiveTV": "Televize",
"HeaderNextUp": "Nadcházející",
"HeaderRecordingGroups": "Skupiny nahrávek",
"HomeVideos": "Domáci videa",
diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json
index 414430ff7..82df43be1 100644
--- a/Emby.Server.Implementations/Localization/Core/de.json
+++ b/Emby.Server.Implementations/Localization/Core/de.json
@@ -3,7 +3,7 @@
"AppDeviceValues": "App: {0}, Gerät: {1}",
"Application": "Anwendung",
"Artists": "Interpreten",
- "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifziert",
+ "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert",
"Books": "Bücher",
"CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
"Channels": "Kanäle",
@@ -99,11 +99,11 @@
"TaskRefreshChannels": "Erneuere Kanäle",
"TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
"TaskCleanTranscode": "Lösche Transkodier Pfad",
- "TaskUpdatePluginsDescription": "Läd Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
+ "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins",
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Erneuere Schausteller",
- "TaskCleanLogsDescription": "Lösche Log Datein die älter als {0} Tage sind.",
+ "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
"TaskRefreshLibrary": "Scanne alle Bibliotheken",
diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json
index 53e2f58de..0753ea39d 100644
--- a/Emby.Server.Implementations/Localization/Core/el.json
+++ b/Emby.Server.Implementations/Localization/Core/el.json
@@ -1,5 +1,5 @@
{
- "Albums": "Άλμπουμ",
+ "Albums": "Άλμπουμς",
"AppDeviceValues": "Εφαρμογή: {0}, Συσκευή: {1}",
"Application": "Εφαρμογή",
"Artists": "Καλλιτέχνες",
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} τελείωσε να παίζει {1} σε {2}",
"ValueHasBeenAddedToLibrary": "{0} προστέθηκαν στη βιβλιοθήκη πολυμέσων σας",
"ValueSpecialEpisodeName": "Σπέσιαλ - {0}",
- "VersionNumber": "Έκδοση {0}"
+ "VersionNumber": "Έκδοση {0}",
+ "TaskRefreshPeople": "Ανανέωση Ατόμων",
+ "TaskCleanLogsDescription": "Διαγράφει τα αρχεία καταγραφής που είναι άνω των {0} ημερών.",
+ "TaskCleanLogs": "Καθαρισμός Καταλόγου Καταγραφής",
+ "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και αναζωογονεί τα μεταδεδομένα.",
+ "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
+ "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο με κεφάλαια.",
+ "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
+ "TaskCleanCacheDescription": "Τα διαγραμμένα αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον από το σύστημα.",
+ "TaskCleanCache": "Καθαρισμός Καταλόγου Προσωρινής Μνήμης",
+ "TasksChannelsCategory": "Κανάλια Διαδικτύου",
+ "TasksApplicationCategory": "Εφαρμογή",
+ "TasksLibraryCategory": "Βιβλιοθήκη",
+ "TasksMaintenanceCategory": "Συντήρηση",
+ "TaskDownloadMissingSubtitlesDescription": "Αναζητήσεις στο διαδίκτυο όπου λείπουν υπότιτλους με βάση τη διαμόρφωση μεταδεδομένων.",
+ "TaskDownloadMissingSubtitles": "Λήψη υπότιτλων που λείπουν",
+ "TaskRefreshChannelsDescription": "Ανανεώνει τις πληροφορίες καναλιού στο διαδικτύου.",
+ "TaskRefreshChannels": "Ανανέωση Καναλιών",
+ "TaskCleanTranscodeDescription": "Διαγράφει αρχείου διακωδικοποιητή περισσότερο από μία ημέρα.",
+ "TaskCleanTranscode": "Καθαρισμός Kαταλόγου Διακωδικοποιητή",
+ "TaskUpdatePluginsDescription": "Κατεβάζει και εγκαθιστά ενημερώσεις για τις προσθήκες που έχουν ρυθμιστεί για αυτόματη ενημέρωση.",
+ "TaskUpdatePlugins": "Ενημέρωση Προσθηκών",
+ "TaskRefreshPeopleDescription": "Ενημερώνει μεταδεδομένα για ηθοποιούς και σκηνοθέτες στην βιβλιοθήκη των πολυμέσων σας."
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 1b6c6b5ae..fc9a10f27 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -24,7 +24,7 @@
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
"HeaderLiveTV": "TV en vivo",
- "HeaderNextUp": "A Continuación",
+ "HeaderNextUp": "Siguiente",
"HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"Inherit": "Heredar",
@@ -44,7 +44,7 @@
"NameInstallFailed": "{0} instalación fallida",
"NameSeasonNumber": "Temporada {0}",
"NameSeasonUnknown": "Temporada desconocida",
- "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
+ "NewVersionIsAvailable": "Una nueva versión del servidor Jellyfin está disponible para descargar.",
"NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
"NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
"NotificationOptionAudioPlayback": "Se inició la reproducción de audio",
@@ -56,7 +56,7 @@
"NotificationOptionPluginInstalled": "Complemento instalado",
"NotificationOptionPluginUninstalled": "Complemento desinstalado",
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
- "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor",
+ "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionTaskFailed": "Falla de tarea programada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionVideoPlayback": "Se inició la reproducción de video",
@@ -71,7 +71,7 @@
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
- "Shows": "Series",
+ "Shows": "Programas",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "El servidor Jellyfin se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
@@ -94,25 +94,25 @@
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
"TaskDownloadMissingSubtitlesDescription": "Busca en internet los subtítulos que falten basándose en la configuración de los metadatos.",
- "TaskDownloadMissingSubtitles": "Descargar subtítulos extraviados",
+ "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
"TaskRefreshChannelsDescription": "Actualizar información de canales de internet.",
"TaskRefreshChannels": "Actualizar canales",
"TaskCleanTranscodeDescription": "Eliminar archivos transcodificados con mas de un día de antigüedad.",
- "TaskCleanTranscode": "Limpiar directorio de Transcodificado",
+ "TaskCleanTranscode": "Limpiar directorio de transcodificación",
"TaskUpdatePluginsDescription": "Descargar e instalar actualizaciones para complementos que estén configurados en actualizar automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos",
- "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su librería multimedia.",
+ "TaskRefreshPeopleDescription": "Actualizar metadatos de actores y directores en su biblioteca multimedia.",
"TaskRefreshPeople": "Actualizar personas",
"TaskCleanLogsDescription": "Eliminar archivos de registro que tengan mas de {0} días de antigüedad.",
"TaskCleanLogs": "Limpiar directorio de registros",
- "TaskRefreshLibraryDescription": "Escanear su librería multimedia por nuevos archivos y refrescar metadatos.",
- "TaskRefreshLibrary": "Escanear librería multimedia",
+ "TaskRefreshLibraryDescription": "Escanear su biblioteca multimedia por nuevos archivos y refrescar metadatos.",
+ "TaskRefreshLibrary": "Escanear biblioteca multimedia",
"TaskRefreshChapterImagesDescription": "Crear miniaturas de videos que tengan capítulos.",
- "TaskRefreshChapterImages": "Extraer imágenes de capitulo",
- "TaskCleanCacheDescription": "Eliminar archivos de cache que no se necesiten en el sistema.",
- "TaskCleanCache": "Limpiar directorio Cache",
- "TasksChannelsCategory": "Canales de Internet",
- "TasksApplicationCategory": "Solicitud",
+ "TaskRefreshChapterImages": "Extraer imágenes de capítulo",
+ "TaskCleanCacheDescription": "Eliminar archivos de caché que no se necesiten en el sistema.",
+ "TaskCleanCache": "Limpiar directorio caché",
+ "TasksChannelsCategory": "Canales de internet",
+ "TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca",
"TasksMaintenanceCategory": "Mantenimiento"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json
index e0bbe90b3..20b37ec9f 100644
--- a/Emby.Server.Implementations/Localization/Core/es-MX.json
+++ b/Emby.Server.Implementations/Localization/Core/es-MX.json
@@ -11,21 +11,21 @@
"Collections": "Colecciones",
"DeviceOfflineWithName": "{0} se ha desconectado",
"DeviceOnlineWithName": "{0} está conectado",
- "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión de {0}",
+ "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
"Favorites": "Favoritos",
"Folders": "Carpetas",
"Genres": "Géneros",
"HeaderAlbumArtists": "Artistas del álbum",
- "HeaderCameraUploads": "Subidos desde Camara",
- "HeaderContinueWatching": "Continuar Viendo",
+ "HeaderCameraUploads": "Subidas desde la cámara",
+ "HeaderContinueWatching": "Continuar viendo",
"HeaderFavoriteAlbums": "Álbumes favoritos",
"HeaderFavoriteArtists": "Artistas favoritos",
"HeaderFavoriteEpisodes": "Episodios favoritos",
"HeaderFavoriteShows": "Programas favoritos",
"HeaderFavoriteSongs": "Canciones favoritas",
- "HeaderLiveTV": "TV en Vivo",
- "HeaderNextUp": "A Continuación",
- "HeaderRecordingGroups": "Grupos de Grabaciones",
+ "HeaderLiveTV": "TV en vivo",
+ "HeaderNextUp": "A continuación",
+ "HeaderRecordingGroups": "Grupos de grabación",
"HomeVideos": "Videos caseros",
"Inherit": "Heredar",
"ItemAddedWithName": "{0} fue agregado a la biblioteca",
@@ -41,12 +41,12 @@
"Movies": "Películas",
"Music": "Música",
"MusicVideos": "Videos musicales",
- "NameInstallFailed": "{0} instalación fallida",
+ "NameInstallFailed": "Falló la instalación de {0}",
"NameSeasonNumber": "Temporada {0}",
- "NameSeasonUnknown": "Temporada Desconocida",
+ "NameSeasonUnknown": "Temporada desconocida",
"NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.",
- "NotificationOptionApplicationUpdateAvailable": "Actualización de aplicación disponible",
- "NotificationOptionApplicationUpdateInstalled": "Actualización de aplicación instalada",
+ "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible",
+ "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada",
"NotificationOptionAudioPlayback": "Reproducción de audio iniciada",
"NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida",
"NotificationOptionCameraImageUploaded": "Imagen de la cámara subida",
@@ -56,7 +56,7 @@
"NotificationOptionPluginInstalled": "Complemento instalado",
"NotificationOptionPluginUninstalled": "Complemento desinstalado",
"NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada",
- "NotificationOptionServerRestartRequired": "Se necesita reiniciar el Servidor",
+ "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor",
"NotificationOptionTaskFailed": "Falla de tarea programada",
"NotificationOptionUserLockedOut": "Usuario bloqueado",
"NotificationOptionVideoPlayback": "Reproducción de video iniciada",
@@ -69,48 +69,48 @@
"PluginUpdatedWithName": "{0} fue actualizado",
"ProviderValue": "Proveedor: {0}",
"ScheduledTaskFailedWithName": "{0} falló",
- "ScheduledTaskStartedWithName": "{0} Iniciado",
+ "ScheduledTaskStartedWithName": "{0} iniciado",
"ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado",
"Shows": "Programas",
"Songs": "Canciones",
- "StartupEmbyServerIsLoading": "El servidor Jellyfin esta cargando. Por favor intente de nuevo dentro de poco.",
+ "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.",
"SubtitleDownloadFailureForItem": "Falló la descarga de subtítulos para {0}",
- "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtitulos desde {0} para {1}",
+ "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}",
"Sync": "Sincronizar",
"System": "Sistema",
"TvShows": "Programas de TV",
"User": "Usuario",
- "UserCreatedWithName": "Se ha creado el usuario {0}",
- "UserDeletedWithName": "Se ha eliminado el usuario {0}",
- "UserDownloadingItemWithValues": "{0} esta descargando {1}",
+ "UserCreatedWithName": "El usuario {0} ha sido creado",
+ "UserDeletedWithName": "El usuario {0} ha sido eliminado",
+ "UserDownloadingItemWithValues": "{0} está descargando {1}",
"UserLockedOutWithName": "El usuario {0} ha sido bloqueado",
"UserOfflineFromDevice": "{0} se ha desconectado desde {1}",
"UserOnlineFromDevice": "{0} está en línea desde {1}",
"UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}",
- "UserPolicyUpdatedWithName": "Las política de usuario ha sido actualizada por {0}",
- "UserStartedPlayingItemWithValues": "{0} está reproduciéndose {1} en {2}",
- "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducirse {1} en {2}",
- "ValueHasBeenAddedToLibrary": "{0} se han añadido a su biblioteca de medios",
+ "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}",
+ "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}",
+ "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}",
+ "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios",
"ValueSpecialEpisodeName": "Especial - {0}",
"VersionNumber": "Versión {0}",
- "TaskDownloadMissingSubtitlesDescription": "Buscar subtítulos de internet basado en configuración de metadatos.",
- "TaskDownloadMissingSubtitles": "Descargar subtítulos perdidos",
- "TaskRefreshChannelsDescription": "Refrescar información de canales de internet.",
+ "TaskDownloadMissingSubtitlesDescription": "Busca subtítulos faltantes en Internet basándose en la configuración de metadatos.",
+ "TaskDownloadMissingSubtitles": "Descargar subtítulos faltantes",
+ "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
"TaskRefreshChannels": "Actualizar canales",
- "TaskCleanTranscodeDescription": "Eliminar archivos transcodificados que tengan mas de un día.",
+ "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
"TaskCleanTranscode": "Limpiar directorio de transcodificado",
- "TaskUpdatePluginsDescription": "Descargar y actualizar complementos que están configurados para actualizarse automáticamente.",
+ "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
"TaskUpdatePlugins": "Actualizar complementos",
- "TaskRefreshPeopleDescription": "Actualizar datos de actores y directores en su librería multimedia.",
- "TaskRefreshPeople": "Refrescar persona",
- "TaskCleanLogsDescription": "Eliminar archivos de registro con mas de {0} días.",
- "TaskCleanLogs": "Directorio de logo limpio",
- "TaskRefreshLibraryDescription": "Escanear su librería multimedia para nuevos archivos y refrescar metadatos.",
- "TaskRefreshLibrary": "Escanear librería multimerdia",
- "TaskRefreshChapterImagesDescription": "Crear miniaturas para videos con capítulos.",
- "TaskRefreshChapterImages": "Extraer imágenes de capítulos",
- "TaskCleanCacheDescription": "Eliminar archivos cache que ya no se necesiten por el sistema.",
- "TaskCleanCache": "Limpiar directorio cache",
+ "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
+ "TaskRefreshPeople": "Actualizar personas",
+ "TaskCleanLogsDescription": "Elimina archivos de registro con más de {0} días de antigüedad.",
+ "TaskCleanLogs": "Limpiar directorio de registros",
+ "TaskRefreshLibraryDescription": "Escanea tu biblioteca de medios por archivos nuevos y actualiza los metadatos.",
+ "TaskRefreshLibrary": "Escanear biblioteca de medios",
+ "TaskRefreshChapterImagesDescription": "Crea miniaturas para videos que tienen capítulos.",
+ "TaskRefreshChapterImages": "Extraer imágenes de los capítulos",
+ "TaskCleanCacheDescription": "Elimina archivos caché que ya no son necesarios para el sistema.",
+ "TaskCleanCache": "Limpiar directorio caché",
"TasksChannelsCategory": "Canales de Internet",
"TasksApplicationCategory": "Aplicación",
"TasksLibraryCategory": "Biblioteca",
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index de1baada8..e7bd3959b 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -71,7 +71,7 @@
"ScheduledTaskFailedWithName": "{0} falló",
"ScheduledTaskStartedWithName": "{0} iniciada",
"ServerNameNeedsToBeRestarted": "{0} necesita ser reiniciado",
- "Shows": "Series",
+ "Shows": "Mostrar",
"Songs": "Canciones",
"StartupEmbyServerIsLoading": "Jellyfin Server se está cargando. Vuelve a intentarlo en breve.",
"SubtitleDownloadFailureForItem": "Error al descargar subtítulos para {0}",
diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json
index be6f87ee3..500c29217 100644
--- a/Emby.Server.Implementations/Localization/Core/fa.json
+++ b/Emby.Server.Implementations/Localization/Core/fa.json
@@ -23,7 +23,7 @@
"HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه",
"HeaderFavoriteShows": "سریال‌های مورد علاقه",
"HeaderFavoriteSongs": "آهنگ‌های مورد علاقه",
- "HeaderLiveTV": "تلویزیون زنده",
+ "HeaderLiveTV": "پخش زنده",
"HeaderNextUp": "قسمت بعدی",
"HeaderRecordingGroups": "گروه‌های ضبط",
"HomeVideos": "ویدیوهای خانگی",
diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json
index b39adefe7..f8d6e0e09 100644
--- a/Emby.Server.Implementations/Localization/Core/fi.json
+++ b/Emby.Server.Implementations/Localization/Core/fi.json
@@ -1,5 +1,5 @@
{
- "HeaderLiveTV": "Suorat lähetykset",
+ "HeaderLiveTV": "Live-TV",
"NewVersionIsAvailable": "Uusi versio Jellyfin palvelimesta on ladattavissa.",
"NameSeasonUnknown": "Tuntematon Kausi",
"NameSeasonNumber": "Kausi {0}",
@@ -12,7 +12,7 @@
"MessageNamedServerConfigurationUpdatedWithValue": "Palvelimen asetusryhmä {0} on päivitetty",
"MessageApplicationUpdatedTo": "Jellyfin palvelin on päivitetty versioon {0}",
"MessageApplicationUpdated": "Jellyfin palvelin on päivitetty",
- "Latest": "Viimeisin",
+ "Latest": "Uusimmat",
"LabelRunningTimeValue": "Toiston kesto: {0}",
"LabelIpAddressValue": "IP-osoite: {0}",
"ItemRemovedWithName": "{0} poistettiin kirjastosta",
@@ -41,7 +41,7 @@
"CameraImageUploadedFrom": "Uusi kamerakuva on ladattu {0}",
"Books": "Kirjat",
"AuthenticationSucceededWithUserName": "{0} todennus onnistui",
- "Artists": "Esiintyjät",
+ "Artists": "Artistit",
"Application": "Sovellus",
"AppDeviceValues": "Sovellus: {0}, Laite: {1}",
"Albums": "Albumit",
@@ -67,21 +67,21 @@
"UserDownloadingItemWithValues": "{0} lataa {1}",
"UserDeletedWithName": "Käyttäjä {0} poistettu",
"UserCreatedWithName": "Käyttäjä {0} luotu",
- "TvShows": "TV-Ohjelmat",
+ "TvShows": "TV-sarjat",
"Sync": "Synkronoi",
- "SubtitleDownloadFailureFromForItem": "Tekstityksen lataaminen epäonnistui {0} - {1}",
+ "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.",
"Songs": "Kappaleet",
- "Shows": "Ohjelmat",
- "ServerNameNeedsToBeRestarted": "{0} vaatii uudelleenkäynnistyksen",
+ "Shows": "Sarjat",
+ "ServerNameNeedsToBeRestarted": "{0} täytyy käynnistää uudelleen",
"ProviderValue": "Tarjoaja: {0}",
"Plugin": "Liitännäinen",
"NotificationOptionVideoPlaybackStopped": "Videon toisto pysäytetty",
- "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
- "NotificationOptionUserLockedOut": "Käyttäjä lukittu",
+ "NotificationOptionVideoPlayback": "Videota toistetaan",
+ "NotificationOptionUserLockedOut": "Käyttäjä kirjautui ulos",
"NotificationOptionTaskFailed": "Ajastettu tehtävä epäonnistui",
- "NotificationOptionServerRestartRequired": "Palvelimen uudelleenkäynnistys vaaditaan",
- "NotificationOptionPluginUpdateInstalled": "Lisäosan päivitys asennettu",
+ "NotificationOptionServerRestartRequired": "Palvelin pitää käynnistää uudelleen",
+ "NotificationOptionPluginUpdateInstalled": "Liitännäinen päivitetty",
"NotificationOptionPluginUninstalled": "Liitännäinen poistettu",
"NotificationOptionPluginInstalled": "Liitännäinen asennettu",
"NotificationOptionPluginError": "Ongelma liitännäisessä",
@@ -90,8 +90,8 @@
"NotificationOptionCameraImageUploaded": "Kameran kuva ladattu",
"NotificationOptionAudioPlaybackStopped": "Äänen toisto lopetettu",
"NotificationOptionAudioPlayback": "Toistetaan ääntä",
- "NotificationOptionApplicationUpdateInstalled": "Uusi sovellusversio asennettu",
- "NotificationOptionApplicationUpdateAvailable": "Sovelluksesta on uusi versio saatavilla",
+ "NotificationOptionApplicationUpdateInstalled": "Sovelluspäivitys asennettu",
+ "NotificationOptionApplicationUpdateAvailable": "Ohjelmistopäivitys saatavilla",
"TasksMaintenanceCategory": "Ylläpito",
"TaskDownloadMissingSubtitlesDescription": "Etsii puuttuvia tekstityksiä videon metadatatietojen pohjalta.",
"TaskDownloadMissingSubtitles": "Lataa puuttuvat tekstitykset",
diff --git a/Emby.Server.Implementations/Localization/Core/fr-CA.json b/Emby.Server.Implementations/Localization/Core/fr-CA.json
index 2c9dae6a1..3dcfa6844 100644
--- a/Emby.Server.Implementations/Localization/Core/fr-CA.json
+++ b/Emby.Server.Implementations/Localization/Core/fr-CA.json
@@ -94,5 +94,24 @@
"ValueSpecialEpisodeName": "Spécial - {0}",
"VersionNumber": "Version {0}",
"TasksLibraryCategory": "Bibliothèque",
- "TasksMaintenanceCategory": "Entretien"
+ "TasksMaintenanceCategory": "Entretien",
+ "TaskDownloadMissingSubtitlesDescription": "Recherche l'internet pour des sous-titres manquants à base de métadonnées configurées.",
+ "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquants",
+ "TaskRefreshChannelsDescription": "Rafraîchit des informations des chaines internet.",
+ "TaskRefreshChannels": "Rafraîchir des chaines",
+ "TaskCleanTranscodeDescription": "Supprime les fichiers de transcodage de plus d'un jour.",
+ "TaskCleanTranscode": "Nettoyer le répertoire de transcodage",
+ "TaskUpdatePluginsDescription": "Télécharger et installer les mises à jours des extensions qui sont configurés pour les m.à.j. automisés.",
+ "TaskUpdatePlugins": "Mise à jour des extensions",
+ "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque de médias.",
+ "TaskRefreshPeople": "Rafraîchir les acteurs",
+ "TaskCleanLogsDescription": "Supprime les journaux qui ont plus que {0} jours.",
+ "TaskCleanLogs": "Nettoyer le répertoire des journaux",
+ "TaskRefreshLibraryDescription": "Analyse votre bibliothèque média pour trouver de nouveaux fichiers et rafraîchit les métadonnées.",
+ "TaskRefreshChapterImages": "Extraire les images de chapitre",
+ "TaskRefreshChapterImagesDescription": "Créer des vignettes pour les vidéos qui ont des chapitres",
+ "TaskRefreshLibrary": "Analyser la bibliothèque de médias",
+ "TaskCleanCache": "Nettoyer le répertoire des fichiers temporaires",
+ "TasksApplicationCategory": "Application",
+ "TaskCleanCacheDescription": "Supprime les fichiers temporaires qui ne sont plus nécessaire pour le système."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index d1403c494..47ebe1254 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -5,17 +5,17 @@
"Artists": "Artistes",
"AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
"Books": "Livres",
- "CameraImageUploadedFrom": "Une nouvelle photographie a été chargée depuis {0}",
+ "CameraImageUploadedFrom": "Une photo a été chargée depuis {0}",
"Channels": "Chaînes",
"ChapterNameValue": "Chapitre {0}",
"Collections": "Collections",
"DeviceOfflineWithName": "{0} s'est déconnecté",
"DeviceOnlineWithName": "{0} est connecté",
- "FailedLoginAttemptWithUserName": "Échec de connexion de {0}",
+ "FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}",
"Favorites": "Favoris",
"Folders": "Dossiers",
"Genres": "Genres",
- "HeaderAlbumArtists": "Artistes d'album",
+ "HeaderAlbumArtists": "Artistes",
"HeaderCameraUploads": "Photos transférées",
"HeaderContinueWatching": "Continuer à regarder",
"HeaderFavoriteAlbums": "Albums favoris",
@@ -69,7 +69,7 @@
"PluginUpdatedWithName": "{0} a été mis à jour",
"ProviderValue": "Fournisseur : {0}",
"ScheduledTaskFailedWithName": "{0} a échoué",
- "ScheduledTaskStartedWithName": "{0} a commencé",
+ "ScheduledTaskStartedWithName": "{0} a démarré",
"ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
"Shows": "Émissions",
"Songs": "Chansons",
@@ -95,21 +95,21 @@
"VersionNumber": "Version {0}",
"TasksChannelsCategory": "Chaines en ligne",
"TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.",
- "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant",
+ "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant",
"TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.",
- "TaskRefreshChannels": "Rafraîchit les chaines",
+ "TaskRefreshChannels": "Rafraîchir les chaines",
"TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.",
- "TaskCleanTranscode": "Nettoie les dossier des transcodages",
- "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.",
- "TaskUpdatePlugins": "Mettre à jour les plugins",
- "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.",
- "TaskRefreshPeople": "Rafraîchit les acteurs",
+ "TaskCleanTranscode": "Nettoyer les dossier des transcodages",
+ "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.",
+ "TaskUpdatePlugins": "Mettre à jour les extensions",
+ "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.",
+ "TaskRefreshPeople": "Rafraîchir les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
- "TaskCleanLogs": "Nettoie le répertoire des journaux",
+ "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": "Scanne toute les Bibliothèques",
+ "TaskRefreshLibrary": "Scanner toute les Bibliothèques",
"TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.",
- "TaskRefreshChapterImages": "Extrait les images de chapitre",
+ "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",
diff --git a/Emby.Server.Implementations/Localization/Core/gsw.json b/Emby.Server.Implementations/Localization/Core/gsw.json
index 9611e33f5..8780a884b 100644
--- a/Emby.Server.Implementations/Localization/Core/gsw.json
+++ b/Emby.Server.Implementations/Localization/Core/gsw.json
@@ -1,41 +1,41 @@
{
- "Albums": "Albom",
- "AppDeviceValues": "App: {0}, Grät: {1}",
- "Application": "Aawändig",
- "Artists": "Könstler",
- "AuthenticationSucceededWithUserName": "{0} het sech aagmäudet",
- "Books": "Büecher",
- "CameraImageUploadedFrom": "Es nöis Foti esch ufeglade worde vo {0}",
- "Channels": "Kanäu",
- "ChapterNameValue": "Kapitu {0}",
- "Collections": "Sammlige",
- "DeviceOfflineWithName": "{0} esch offline gange",
- "DeviceOnlineWithName": "{0} esch online cho",
- "FailedLoginAttemptWithUserName": "Fäugschlagne Aamäudeversuech vo {0}",
- "Favorites": "Favorite",
+ "Albums": "Alben",
+ "AppDeviceValues": "App: {0}, Gerät: {1}",
+ "Application": "Anwendung",
+ "Artists": "Künstler",
+ "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
+ "Books": "Bücher",
+ "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen",
+ "Channels": "Kanäle",
+ "ChapterNameValue": "Kapitel {0}",
+ "Collections": "Sammlungen",
+ "DeviceOfflineWithName": "{0} wurde getrennt",
+ "DeviceOnlineWithName": "{0} ist verbunden",
+ "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
+ "Favorites": "Favoriten",
"Folders": "Ordner",
"Genres": "Genres",
- "HeaderAlbumArtists": "Albom-Könstler",
+ "HeaderAlbumArtists": "Album-Künstler",
"HeaderCameraUploads": "Kamera-Uploads",
- "HeaderContinueWatching": "Wiiterluege",
- "HeaderFavoriteAlbums": "Lieblingsalbe",
- "HeaderFavoriteArtists": "Lieblings-Interprete",
- "HeaderFavoriteEpisodes": "Lieblingsepisode",
- "HeaderFavoriteShows": "Lieblingsserie",
+ "HeaderContinueWatching": "weiter schauen",
+ "HeaderFavoriteAlbums": "Lieblingsalben",
+ "HeaderFavoriteArtists": "Lieblings-Künstler",
+ "HeaderFavoriteEpisodes": "Lieblingsepisoden",
+ "HeaderFavoriteShows": "Lieblingsserien",
"HeaderFavoriteSongs": "Lieblingslieder",
- "HeaderLiveTV": "Live-Färnseh",
- "HeaderNextUp": "Als nächts",
- "HeaderRecordingGroups": "Ufnahmegruppe",
- "HomeVideos": "Heimfilmli",
- "Inherit": "Hinzuefüege",
- "ItemAddedWithName": "{0} esch de Bibliothek dezuegfüegt worde",
- "ItemRemovedWithName": "{0} esch vo de Bibliothek entfärnt worde",
- "LabelIpAddressValue": "IP-Adrässe: {0}",
- "LabelRunningTimeValue": "Loufziit: {0}",
- "Latest": "Nöischti",
- "MessageApplicationUpdated": "Jellyfin Server esch aktualisiert worde",
- "MessageApplicationUpdatedTo": "Jellyfin Server esch of Version {0} aktualisiert worde",
- "MessageNamedServerConfigurationUpdatedWithValue": "De Serveriistöuigsberiich {0} esch aktualisiert worde",
+ "HeaderLiveTV": "Live-Fernseh",
+ "HeaderNextUp": "Als Nächstes",
+ "HeaderRecordingGroups": "Aufnahme-Gruppen",
+ "HomeVideos": "Heimvideos",
+ "Inherit": "Vererben",
+ "ItemAddedWithName": "{0} wurde der Bibliothek hinzugefügt",
+ "ItemRemovedWithName": "{0} wurde aus der Bibliothek entfernt",
+ "LabelIpAddressValue": "IP-Adresse: {0}",
+ "LabelRunningTimeValue": "Laufzeit: {0}",
+ "Latest": "Neueste",
+ "MessageApplicationUpdated": "Jellyfin-Server wurde aktualisiert",
+ "MessageApplicationUpdatedTo": "Jellyfin-Server wurde auf Version {0} aktualisiert",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Der Server-Einstellungsbereich {0} wurde aktualisiert",
"MessageServerConfigurationUpdated": "Serveriistöuige send aktualisiert worde",
"MixedContent": "Gmeschti Inhäut",
"Movies": "Film",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
"NotificationOptionCameraImageUploaded": "Foti ueglade",
- "NotificationOptionInstallationFailed": "Installationsfäuer",
+ "NotificationOptionInstallationFailed": "Installationsfehler",
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
"NotificationOptionPluginError": "Plugin-Fäuer",
"NotificationOptionPluginInstalled": "Plugin installiert",
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} het d'Wedergab vo {1} of {2} gstoppt",
"ValueHasBeenAddedToLibrary": "{0} esch dinnere Biblithek hinzuegfüegt worde",
"ValueSpecialEpisodeName": "Extra - {0}",
- "VersionNumber": "Version {0}"
+ "VersionNumber": "Version {0}",
+ "TaskCleanLogs": "Lösche Log Pfad",
+ "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",
+ "TaskRefreshLibrary": "Scanne alle Bibliotheken",
+ "TaskRefreshChapterImagesDescription": "Kreiert Vorschaubilder für Videos welche Kapitel haben.",
+ "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
+ "TaskCleanCacheDescription": "Löscht Zwischenspeicherdatein die nicht länger von System gebraucht werden.",
+ "TaskCleanCache": "Leere Cache Pfad",
+ "TasksChannelsCategory": "Internet Kanäle",
+ "TasksApplicationCategory": "Applikation",
+ "TasksLibraryCategory": "Bibliothek",
+ "TasksMaintenanceCategory": "Verwaltung",
+ "TaskDownloadMissingSubtitlesDescription": "Durchsucht das Internet nach fehlenden Untertiteln, basierend auf den Metadaten Einstellungen.",
+ "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
+ "TaskRefreshChannelsDescription": "Aktualisiert Internet Kanal Informationen.",
+ "TaskRefreshChannels": "Aktualisiere Kanäle",
+ "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.",
+ "TaskCleanTranscode": "Räume Transcodier Verzeichnis auf",
+ "TaskUpdatePluginsDescription": "Lädt Aktualisierungen für Erweiterungen herunter und installiert diese, für welche automatische Aktualisierungen konfiguriert sind.",
+ "TaskUpdatePlugins": "Aktualisiere Erweiterungen",
+ "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schausteller und Regisseure in deiner Bibliothek.",
+ "TaskRefreshPeople": "Aktualisiere Schauspieler",
+ "TaskCleanLogsDescription": "Löscht Log Dateien die älter als {0} Tage sind."
}
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 1ce8b08a0..682f5325b 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -1,7 +1,7 @@
{
"Albums": "אלבומים",
"AppDeviceValues": "יישום: {0}, מכשיר: {1}",
- "Application": "אפליקציה",
+ "Application": "יישום",
"Artists": "אומנים",
"AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
"Books": "ספרים",
@@ -62,7 +62,7 @@
"NotificationOptionVideoPlayback": "Video playback started",
"NotificationOptionVideoPlaybackStopped": "Video playback stopped",
"Photos": "תמונות",
- "Playlists": "רשימות ניגון",
+ "Playlists": "רשימות הפעלה",
"Plugin": "Plugin",
"PluginInstalledWithName": "{0} was installed",
"PluginUninstalledWithName": "{0} was uninstalled",
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "מיוחד- {0}",
- "VersionNumber": "Version {0}"
+ "VersionNumber": "Version {0}",
+ "TaskRefreshLibrary": "סרוק ספריית מדיה",
+ "TaskRefreshChapterImages": "חלץ תמונות פרקים",
+ "TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.",
+ "TaskCleanCache": "נקה תיקיית מטמון",
+ "TasksApplicationCategory": "יישום",
+ "TasksLibraryCategory": "ספרייה",
+ "TasksMaintenanceCategory": "תחזוקה",
+ "TaskUpdatePlugins": "עדכן תוספים",
+ "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+ "TaskRefreshPeople": "רענן אנשים",
+ "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
+ "TaskCleanLogs": "נקה תיקיית יומן",
+ "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+ "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
+ "TasksChannelsCategory": "ערוצי אינטרנט",
+ "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
+ "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות.",
+ "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
+ "TaskRefreshChannels": "רענן ערוץ",
+ "TaskCleanTranscodeDescription": "מחק קבצי transcode שנוצרו מלפני יותר מיום.",
+ "TaskCleanTranscode": "נקה תקיית Transcode",
+ "TaskUpdatePluginsDescription": "הורד והתקן עדכונים עבור תוספים שמוגדרים לעדכון אוטומטי."
}
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 6947178d7..c169a35e7 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -30,7 +30,7 @@
"Inherit": "Naslijedi",
"ItemAddedWithName": "{0} je dodano u biblioteku",
"ItemRemovedWithName": "{0} je uklonjen iz biblioteke",
- "LabelIpAddressValue": "Ip adresa: {0}",
+ "LabelIpAddressValue": "IP adresa: {0}",
"LabelRunningTimeValue": "Vrijeme rada: {0}",
"Latest": "Najnovije",
"MessageApplicationUpdated": "Jellyfin Server je ažuriran",
@@ -92,5 +92,13 @@
"UserStoppedPlayingItemWithValues": "{0} je zaustavio {1}",
"ValueHasBeenAddedToLibrary": "{0} has been added to your media library",
"ValueSpecialEpisodeName": "Specijal - {0}",
- "VersionNumber": "Verzija {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",
+ "TasksApplicationCategory": "Aplikacija",
+ "TasksMaintenanceCategory": "Održavanje"
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index ef2a57e8e..0f0f9130b 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -80,16 +80,32 @@
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
"UserStartedPlayingItemWithValues": "{0} er að spila {1} á {2}",
- "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir notanda {0}",
+ "UserPolicyUpdatedWithName": "Notandaregla hefur verið uppfærð fyrir {0}",
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
- "UserLockedOutWithName": "Notanda {0} hefur verið hindraður aðgangur",
+ "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
"UserDownloadingItemWithValues": "{0} Hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"ProviderValue": "Veitandi: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
"ValueSpecialEpisodeName": "Sérstakt - {0}",
- "Shows": "Þættir",
- "Playlists": "Spilunarlisti"
+ "Shows": "Sýningar",
+ "Playlists": "Spilunarlisti",
+ "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
+ "TaskRefreshChannels": "Endurhlaða Rásir",
+ "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
+ "TaskCleanTranscode": "Hreinsa Umkóðunarmöppu",
+ "TaskUpdatePluginsDescription": "Sækja og setja upp uppfærslur fyrir viðbætur sem eru stilltar til að uppfæra sjálfkrafa.",
+ "TaskUpdatePlugins": "Uppfæra viðbætur",
+ "TaskRefreshPeopleDescription": "Uppfærir lýsigögn fyrir leikara og leikstjóra í miðlasafninu þínu.",
+ "TaskRefreshLibraryDescription": "Skannar miðlasafnið þitt fyrir nýjum skrám og uppfærir lýsigögn.",
+ "TaskRefreshLibrary": "Skanna miðlasafn",
+ "TaskRefreshChapterImagesDescription": "Býr til smámyndir fyrir myndbönd sem hafa kaflaskil.",
+ "TaskCleanCacheDescription": "Eyðir skrám í skyndiminni sem ekki er lengur þörf fyrir í kerfinu.",
+ "TaskCleanCache": "Hreinsa skráasafn skyndiminnis",
+ "TasksChannelsCategory": "Netrásir",
+ "TasksApplicationCategory": "Forrit",
+ "TasksLibraryCategory": "Miðlasafn",
+ "TasksMaintenanceCategory": "Viðhald"
}
diff --git a/Emby.Server.Implementations/Localization/Core/it.json b/Emby.Server.Implementations/Localization/Core/it.json
index 0758bbe9c..7f5a56e86 100644
--- a/Emby.Server.Implementations/Localization/Core/it.json
+++ b/Emby.Server.Implementations/Localization/Core/it.json
@@ -5,7 +5,7 @@
"Artists": "Artisti",
"AuthenticationSucceededWithUserName": "{0} autenticato con successo",
"Books": "Libri",
- "CameraImageUploadedFrom": "È stata caricata una nuova immagine della fotocamera dal device {0}",
+ "CameraImageUploadedFrom": "È stata caricata una nuova fotografia da {0}",
"Channels": "Canali",
"ChapterNameValue": "Capitolo {0}",
"Collections": "Collezioni",
diff --git a/Emby.Server.Implementations/Localization/Core/ja.json b/Emby.Server.Implementations/Localization/Core/ja.json
index 5e017d4c4..a4d9f9ef6 100644
--- a/Emby.Server.Implementations/Localization/Core/ja.json
+++ b/Emby.Server.Implementations/Localization/Core/ja.json
@@ -104,13 +104,14 @@
"TasksMaintenanceCategory": "メンテナンス",
"TaskRefreshChannelsDescription": "ネットチャンネルの情報をリフレッシュします。",
"TaskRefreshChannels": "チャンネルのリフレッシュ",
- "TaskCleanTranscodeDescription": "一日以上前のトランスコードを消去します。",
- "TaskCleanTranscode": "トランスコード用のディレクトリの掃除",
+ "TaskCleanTranscodeDescription": "1日以上経過したトランスコードファイルを削除します。",
+ "TaskCleanTranscode": "トランスコードディレクトリの削除",
"TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
"TaskUpdatePlugins": "プラグインの更新",
- "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータをリフレッシュします。",
- "TaskRefreshPeople": "俳優や監督のデータのリフレッシュ",
+ "TaskRefreshPeopleDescription": "メディアライブラリで俳優や監督のメタデータを更新します。",
+ "TaskRefreshPeople": "俳優や監督のデータの更新",
"TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索します。",
"TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
- "TaskRefreshChapterImages": "チャプター画像を抽出する"
+ "TaskRefreshChapterImages": "チャプター画像を抽出する",
+ "TaskDownloadMissingSubtitles": "不足している字幕をダウンロードする"
}
diff --git a/Emby.Server.Implementations/Localization/Core/lt-LT.json b/Emby.Server.Implementations/Localization/Core/lt-LT.json
index 01a740187..35053766b 100644
--- a/Emby.Server.Implementations/Localization/Core/lt-LT.json
+++ b/Emby.Server.Implementations/Localization/Core/lt-LT.json
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
"ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
"ValueSpecialEpisodeName": "Ypatinga - {0}",
- "VersionNumber": "Version {0}"
+ "VersionNumber": "Version {0}",
+ "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
+ "TaskUpdatePlugins": "Atnaujinti Priedus",
+ "TaskDownloadMissingSubtitlesDescription": "Ieško internete trūkstamų subtitrų remiantis metaduomenų konfigūracija.",
+ "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
+ "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
+ "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
+ "TaskRefreshLibrary": "Skenuoti Mediateka",
+ "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
+ "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informacija.",
+ "TaskRefreshChannels": "Atnaujinti Kanalus",
+ "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
+ "TaskRefreshPeople": "Atnaujinti Žmones",
+ "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
+ "TaskCleanLogs": "Išvalyti Žurnalą",
+ "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
+ "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
+ "TaskCleanCache": "Išvalyti Talpyklą",
+ "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
+ "TasksChannelsCategory": "Internetiniai Kanalai",
+ "TasksApplicationCategory": "Programa",
+ "TasksLibraryCategory": "Mediateka",
+ "TasksMaintenanceCategory": "Priežiūra"
}
diff --git a/Emby.Server.Implementations/Localization/Core/mk.json b/Emby.Server.Implementations/Localization/Core/mk.json
index 8df137302..bbdf99aba 100644
--- a/Emby.Server.Implementations/Localization/Core/mk.json
+++ b/Emby.Server.Implementations/Localization/Core/mk.json
@@ -91,5 +91,12 @@
"Songs": "Песни",
"Shows": "Серии",
"ServerNameNeedsToBeRestarted": "{0} треба да се рестартира",
- "ScheduledTaskStartedWithName": "{0} започна"
+ "ScheduledTaskStartedWithName": "{0} започна",
+ "TaskRefreshChapterImages": "Извези Слики од Поглавје",
+ "TaskCleanCacheDescription": "Ги брише кешираните фајлови што не се повеќе потребни од системот.",
+ "TaskCleanCache": "Исчисти Го Кешот",
+ "TasksChannelsCategory": "Интернет Канали",
+ "TasksApplicationCategory": "Апликација",
+ "TasksLibraryCategory": "Библиотека",
+ "TasksMaintenanceCategory": "Одржување"
}
diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json
index e523ae90b..5637ce346 100644
--- a/Emby.Server.Implementations/Localization/Core/nb.json
+++ b/Emby.Server.Implementations/Localization/Core/nb.json
@@ -96,5 +96,10 @@
"TasksChannelsCategory": "Internett kanaler",
"TasksApplicationCategory": "Applikasjon",
"TasksLibraryCategory": "Bibliotek",
- "TasksMaintenanceCategory": "Vedlikehold"
+ "TasksMaintenanceCategory": "Vedlikehold",
+ "TaskCleanCache": "Tøm buffer katalog",
+ "TaskRefreshLibrary": "Skann mediebibliotek",
+ "TaskRefreshChapterImagesDescription": "Lager forhåndsvisningsbilder for videoer som har kapitler.",
+ "TaskRefreshChapterImages": "Trekk ut Kapittelbilder",
+ "TaskCleanCacheDescription": "Sletter mellomlagrede filer som ikke lengre trengs av systemet."
}
diff --git a/Emby.Server.Implementations/Localization/Core/nl.json b/Emby.Server.Implementations/Localization/Core/nl.json
index 3bc9c2a77..41c74d54d 100644
--- a/Emby.Server.Implementations/Localization/Core/nl.json
+++ b/Emby.Server.Implementations/Localization/Core/nl.json
@@ -1,11 +1,11 @@
{
"Albums": "Albums",
"AppDeviceValues": "App: {0}, Apparaat: {1}",
- "Application": "Applicatie",
+ "Application": "Programma",
"Artists": "Artiesten",
- "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
+ "AuthenticationSucceededWithUserName": "{0} is succesvol geverifieerd",
"Books": "Boeken",
- "CameraImageUploadedFrom": "Er is een nieuwe foto toegevoegd van {0}",
+ "CameraImageUploadedFrom": "Er is een nieuwe camera afbeelding toegevoegd via {0}",
"Channels": "Kanalen",
"ChapterNameValue": "Hoofdstuk {0}",
"Collections": "Verzamelingen",
@@ -26,7 +26,7 @@
"HeaderLiveTV": "Live TV",
"HeaderNextUp": "Volgende",
"HeaderRecordingGroups": "Opnamegroepen",
- "HomeVideos": "Start video's",
+ "HomeVideos": "Home video's",
"Inherit": "Overerven",
"ItemAddedWithName": "{0} is toegevoegd aan de bibliotheek",
"ItemRemovedWithName": "{0} is verwijderd uit de bibliotheek",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Muziek gestart",
"NotificationOptionAudioPlaybackStopped": "Muziek gestopt",
"NotificationOptionCameraImageUploaded": "Camera-afbeelding geüpload",
- "NotificationOptionInstallationFailed": "Installatie mislukking",
+ "NotificationOptionInstallationFailed": "Installatie mislukt",
"NotificationOptionNewLibraryContent": "Nieuwe content toegevoegd",
"NotificationOptionPluginError": "Plug-in fout",
"NotificationOptionPluginInstalled": "Plug-in geïnstalleerd",
diff --git a/Emby.Server.Implementations/Localization/Core/pl.json b/Emby.Server.Implementations/Localization/Core/pl.json
index e9d9bbf2e..bdc0d0169 100644
--- a/Emby.Server.Implementations/Localization/Core/pl.json
+++ b/Emby.Server.Implementations/Localization/Core/pl.json
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} zakończył odtwarzanie {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} został dodany do biblioteki mediów",
"ValueSpecialEpisodeName": "Specjalne - {0}",
- "VersionNumber": "Wersja {0}"
+ "VersionNumber": "Wersja {0}",
+ "TaskDownloadMissingSubtitlesDescription": "Przeszukuje internet w poszukiwaniu brakujących napisów w oparciu o konfigurację metadanych.",
+ "TaskDownloadMissingSubtitles": "Pobierz brakujące napisy",
+ "TaskRefreshChannelsDescription": "Odświeża informacje o kanałach internetowych.",
+ "TaskRefreshChannels": "Odśwież kanały",
+ "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
+ "TaskCleanTranscode": "Wyczyść folder transkodowania",
+ "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.",
+ "TaskUpdatePlugins": "Aktualizuj pluginy",
+ "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
+ "TaskRefreshPeople": "Odśwież obsadę",
+ "TaskCleanLogsDescription": "Kasuje pliki logów starsze niż {0} dni.",
+ "TaskCleanLogs": "Wyczyść folder logów",
+ "TaskRefreshLibraryDescription": "Skanuje Twoją bibliotekę mediów dla nowych plików i odświeżenia metadanych.",
+ "TaskRefreshLibrary": "Skanuj bibliotekę mediów",
+ "TaskRefreshChapterImagesDescription": "Tworzy miniatury dla filmów posiadających rozdziały.",
+ "TaskRefreshChapterImages": "Wydobądź grafiki rozdziałów",
+ "TaskCleanCacheDescription": "Usuwa niepotrzebne i przestarzałe pliki cache.",
+ "TaskCleanCache": "Wyczyść folder Cache",
+ "TasksChannelsCategory": "Kanały internetowe",
+ "TasksApplicationCategory": "Aplikacja",
+ "TasksLibraryCategory": "Biblioteka",
+ "TasksMaintenanceCategory": "Konserwacja"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt-BR.json b/Emby.Server.Implementations/Localization/Core/pt-BR.json
index 3a69b6d7a..275195640 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-BR.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-BR.json
@@ -19,10 +19,10 @@
"HeaderCameraUploads": "Envios da Câmera",
"HeaderContinueWatching": "Continuar Assistindo",
"HeaderFavoriteAlbums": "Álbuns Favoritos",
- "HeaderFavoriteArtists": "Artistas Favoritos",
- "HeaderFavoriteEpisodes": "Episódios Favoritos",
- "HeaderFavoriteShows": "Séries Favoritas",
- "HeaderFavoriteSongs": "Músicas Favoritas",
+ "HeaderFavoriteArtists": "Artistas favoritos",
+ "HeaderFavoriteEpisodes": "Episódios favoritos",
+ "HeaderFavoriteShows": "Séries favoritas",
+ "HeaderFavoriteSongs": "Músicas favoritas",
"HeaderLiveTV": "TV ao Vivo",
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json
index ebf35c492..c1fb65743 100644
--- a/Emby.Server.Implementations/Localization/Core/pt-PT.json
+++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json
@@ -26,7 +26,7 @@
"HeaderLiveTV": "TV em Direto",
"HeaderNextUp": "A Seguir",
"HeaderRecordingGroups": "Grupos de Gravação",
- "HomeVideos": "Home videos",
+ "HomeVideos": "Videos caseiros",
"Inherit": "Herdar",
"ItemAddedWithName": "{0} foi adicionado à biblioteca",
"ItemRemovedWithName": "{0} foi removido da biblioteca",
@@ -92,5 +92,27 @@
"UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}",
"ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia",
"ValueSpecialEpisodeName": "Especial - {0}",
- "VersionNumber": "Versão {0}"
+ "VersionNumber": "Versão {0}",
+ "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.",
+ "TaskDownloadMissingSubtitles": "Fazer download de legendas em falta",
+ "TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.",
+ "TaskRefreshChannels": "Atualizar Canais",
+ "TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.",
+ "TaskCleanTranscode": "Limpar a Diretoria de Transcode",
+ "TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.",
+ "TaskUpdatePlugins": "Atualizar Plugins",
+ "TaskRefreshPeopleDescription": "Atualizar metadados para atores e diretores na biblioteca.",
+ "TaskRefreshPeople": "Atualizar Pessoas",
+ "TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.",
+ "TaskCleanLogs": "Limpar a Diretoria de Logs",
+ "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.",
+ "TaskRefreshLibrary": "Scannear Biblioteca de Música",
+ "TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.",
+ "TaskRefreshChapterImages": "Extrair Imagens dos Capítulos",
+ "TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.",
+ "TaskCleanCache": "Limpar Cache",
+ "TasksChannelsCategory": "Canais da Internet",
+ "TasksApplicationCategory": "Aplicação",
+ "TasksLibraryCategory": "Biblioteca",
+ "TasksMaintenanceCategory": "Manutenção"
}
diff --git a/Emby.Server.Implementations/Localization/Core/pt.json b/Emby.Server.Implementations/Localization/Core/pt.json
index 661ee8603..25c5b9053 100644
--- a/Emby.Server.Implementations/Localization/Core/pt.json
+++ b/Emby.Server.Implementations/Localization/Core/pt.json
@@ -95,5 +95,13 @@
"TaskCleanCache": "Limpar Diretório de Cache",
"TasksApplicationCategory": "Aplicação",
"TasksLibraryCategory": "Biblioteca",
- "TasksMaintenanceCategory": "Manutenção"
+ "TasksMaintenanceCategory": "Manutenção",
+ "TaskRefreshChannels": "Atualizar Canais",
+ "TaskUpdatePlugins": "Atualizar Plugins",
+ "TaskCleanLogsDescription": "Deletar arquivos de log que existe a mais de {0} dias.",
+ "TaskCleanLogs": "Limpar diretório de log",
+ "TaskRefreshLibrary": "Escanear biblioteca de mídias",
+ "TaskRefreshChapterImagesDescription": "Criar miniaturas para videos que tem capítulos.",
+ "TaskCleanCacheDescription": "Deletar arquivos de cache que não são mais usados pelo sistema.",
+ "TasksChannelsCategory": "Canais de Internet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json
index c46aa5c30..71ee6446c 100644
--- a/Emby.Server.Implementations/Localization/Core/ru.json
+++ b/Emby.Server.Implementations/Localization/Core/ru.json
@@ -9,8 +9,8 @@
"Channels": "Каналы",
"ChapterNameValue": "Сцена {0}",
"Collections": "Коллекции",
- "DeviceOfflineWithName": "{0} - подкл. разъ-но",
- "DeviceOnlineWithName": "{0} - подкл. уст-но",
+ "DeviceOfflineWithName": "{0} - отключено",
+ "DeviceOnlineWithName": "{0} - подключено",
"FailedLoginAttemptWithUserName": "{0} - попытка входа неудачна",
"Favorites": "Избранное",
"Folders": "Папки",
@@ -26,30 +26,30 @@
"HeaderLiveTV": "Эфир",
"HeaderNextUp": "Очередное",
"HeaderRecordingGroups": "Группы записей",
- "HomeVideos": "Дом. видео",
+ "HomeVideos": "Домашнее видео",
"Inherit": "Наследуемое",
"ItemAddedWithName": "{0} - добавлено в медиатеку",
"ItemRemovedWithName": "{0} - изъято из медиатеки",
"LabelIpAddressValue": "IP-адрес: {0}",
"LabelRunningTimeValue": "Длительность: {0}",
- "Latest": "Новейшее",
+ "Latest": "Последнее",
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Конфиг-ия сервера (раздел {0}) была обновлена",
- "MessageServerConfigurationUpdated": "Конфиг-ия сервера была обновлена",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
+ "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
"MixedContent": "Смешанное содержимое",
"Movies": "Кино",
"Music": "Музыка",
- "MusicVideos": "Муз. видео",
+ "MusicVideos": "Музыкальные клипы",
"NameInstallFailed": "Установка {0} неудачна",
"NameSeasonNumber": "Сезон {0}",
"NameSeasonUnknown": "Сезон неопознан",
"NewVersionIsAvailable": "Новая версия Jellyfin Server доступна для загрузки.",
"NotificationOptionApplicationUpdateAvailable": "Имеется обновление приложения",
"NotificationOptionApplicationUpdateInstalled": "Обновление приложения установлено",
- "NotificationOptionAudioPlayback": "Воспр-ие аудио зап-но",
- "NotificationOptionAudioPlaybackStopped": "Восп-ие аудио ост-но",
- "NotificationOptionCameraImageUploaded": "Произведена выкладка отснятого с камеры",
+ "NotificationOptionAudioPlayback": "Воспроизведение аудио запущено",
+ "NotificationOptionAudioPlaybackStopped": "Воспроизведение аудио остановлено",
+ "NotificationOptionCameraImageUploaded": "Изображения с камеры загружены",
"NotificationOptionInstallationFailed": "Сбой установки",
"NotificationOptionNewLibraryContent": "Новое содержание добавлено",
"NotificationOptionPluginError": "Сбой плагина",
@@ -59,8 +59,8 @@
"NotificationOptionServerRestartRequired": "Требуется перезапуск сервера",
"NotificationOptionTaskFailed": "Сбой назначенной задачи",
"NotificationOptionUserLockedOut": "Пользователь заблокирован",
- "NotificationOptionVideoPlayback": "Воспр-ие видео зап-но",
- "NotificationOptionVideoPlaybackStopped": "Восп-ие видео ост-но",
+ "NotificationOptionVideoPlayback": "Воспроизведение видео запущено",
+ "NotificationOptionVideoPlaybackStopped": "Воспроизведение видео остановлено",
"Photos": "Фото",
"Playlists": "Плей-листы",
"Plugin": "Плагин",
@@ -76,21 +76,43 @@
"StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
"SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
- "Sync": "Синхро",
+ "Sync": "Синхронизация",
"System": "Система",
"TvShows": "ТВ",
- "User": "Польз-ль",
+ "User": "Пользователь",
"UserCreatedWithName": "Пользователь {0} был создан",
"UserDeletedWithName": "Пользователь {0} был удалён",
"UserDownloadingItemWithValues": "{0} загружает {1}",
"UserLockedOutWithName": "Пользователь {0} был заблокирован",
- "UserOfflineFromDevice": "{0} - подкл. с {1} разъ-но",
- "UserOnlineFromDevice": "{0} - подкл. с {1} уст-но",
- "UserPasswordChangedWithName": "Пароль польз-ля {0} был изменён",
- "UserPolicyUpdatedWithName": "Польз-ие политики {0} были обновлены",
- "UserStartedPlayingItemWithValues": "{0} - воспр. «{1}» на {2}",
- "UserStoppedPlayingItemWithValues": "{0} - воспр. «{1}» ост-но на {2}",
+ "UserOfflineFromDevice": "{0} отключился с {1}",
+ "UserOnlineFromDevice": "{0} подключился с {1}",
+ "UserPasswordChangedWithName": "Пароль пользователя {0} был изменён",
+ "UserPolicyUpdatedWithName": "Политики пользователя {0} были обновлены",
+ "UserStartedPlayingItemWithValues": "{0} - воспроизведение «{1}» на {2}",
+ "UserStoppedPlayingItemWithValues": "{0} - воспроизведение остановлено «{1}» на {2}",
"ValueHasBeenAddedToLibrary": "{0} (добавлено в медиатеку)",
- "ValueSpecialEpisodeName": "Спецэпизод - {0}",
- "VersionNumber": "Версия {0}"
+ "ValueSpecialEpisodeName": "Специальный эпизод - {0}",
+ "VersionNumber": "Версия {0}",
+ "TaskDownloadMissingSubtitles": "Загрузка отсутствующих субтитров",
+ "TaskRefreshChannels": "Обновление каналов",
+ "TaskCleanTranscode": "Очистка каталога перекодировки",
+ "TaskUpdatePlugins": "Обновление плагинов",
+ "TaskRefreshPeople": "Обновление метаданных людей",
+ "TaskCleanLogs": "Очистка каталога журналов",
+ "TaskRefreshLibrary": "Сканирование медиатеки",
+ "TaskRefreshChapterImages": "Извлечение изображений сцен",
+ "TaskCleanCache": "Очистка каталога кеша",
+ "TasksChannelsCategory": "Интернет-каналы",
+ "TasksApplicationCategory": "Приложение",
+ "TasksLibraryCategory": "Медиатека",
+ "TasksMaintenanceCategory": "Обслуживание",
+ "TaskDownloadMissingSubtitlesDescription": "Выполняется поиск в Интернете отсутствующих субтитров на основе конфигурации метаданных.",
+ "TaskRefreshChannelsDescription": "Обновляются данные интернет-каналов.",
+ "TaskCleanTranscodeDescription": "Удаляются файлы перекодировки старше одного дня.",
+ "TaskUpdatePluginsDescription": "Загружаются и устанавливаются обновления для плагинов, у которых включено автоматическое обновление.",
+ "TaskRefreshPeopleDescription": "Обновляются метаданные актеров и режиссёров в медиатеке.",
+ "TaskCleanLogsDescription": "Удаляются файлы журнала, возраст которых превышает {0} дн(я/ей).",
+ "TaskRefreshLibraryDescription": "Сканируется медиатека на новые файлы и обновляются метаданные.",
+ "TaskRefreshChapterImagesDescription": "Создаются эскизы для видео, которые содержат сцены.",
+ "TaskCleanCacheDescription": "Удаляются файлы кэша, которые больше не нужны системе."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sl-SI.json b/Emby.Server.Implementations/Localization/Core/sl-SI.json
index b60dd33bd..60c58d472 100644
--- a/Emby.Server.Implementations/Localization/Core/sl-SI.json
+++ b/Emby.Server.Implementations/Localization/Core/sl-SI.json
@@ -92,5 +92,26 @@
"UserStoppedPlayingItemWithValues": "{0} je nehal predvajati {1} na {2}",
"ValueHasBeenAddedToLibrary": "{0} je bil dodan vaši knjižnici",
"ValueSpecialEpisodeName": "Poseben - {0}",
- "VersionNumber": "Različica {0}"
+ "VersionNumber": "Različica {0}",
+ "TaskDownloadMissingSubtitles": "Prenesi manjkajoče podnapise",
+ "TaskRefreshChannelsDescription": "Osveži podatke spletnih kanalov.",
+ "TaskRefreshChannels": "Osveži kanale",
+ "TaskCleanTranscodeDescription": "Izbriše več kot dan stare datoteke prekodiranja.",
+ "TaskCleanTranscode": "Počisti mapo prekodiranja",
+ "TaskUpdatePluginsDescription": "Prenese in namesti posodobitve za dodatke, ki imajo omogočene samodejne posodobitve.",
+ "TaskUpdatePlugins": "Posodobi dodatke",
+ "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",
+ "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.",
+ "TaskRefreshChapterImages": "Izvleči slike poglavij",
+ "TaskCleanCacheDescription": "Izbriše predpomnjene datoteke, ki niso več potrebne.",
+ "TaskCleanCache": "Počisti mapo predpomnilnika",
+ "TasksChannelsCategory": "Spletni kanali",
+ "TasksApplicationCategory": "Aplikacija",
+ "TasksLibraryCategory": "Knjižnica",
+ "TasksMaintenanceCategory": "Vzdrževanje"
}
diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json
index b7c50394a..c8662b2ca 100644
--- a/Emby.Server.Implementations/Localization/Core/sv.json
+++ b/Emby.Server.Implementations/Localization/Core/sv.json
@@ -9,7 +9,7 @@
"Channels": "Kanaler",
"ChapterNameValue": "Kapitel {0}",
"Collections": "Samlingar",
- "DeviceOfflineWithName": "{0} har tappat anslutningen",
+ "DeviceOfflineWithName": "{0} har kopplat från",
"DeviceOnlineWithName": "{0} är ansluten",
"FailedLoginAttemptWithUserName": "Misslyckat inloggningsförsök från {0}",
"Favorites": "Favoriter",
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
"NotificationOptionAudioPlaybackStopped": "Ljuduppspelning stoppades",
"NotificationOptionCameraImageUploaded": "Kamerabild har laddats upp",
- "NotificationOptionInstallationFailed": "Fel vid installation",
+ "NotificationOptionInstallationFailed": "Installationen misslyckades",
"NotificationOptionNewLibraryContent": "Nytt innehåll har lagts till",
"NotificationOptionPluginError": "Fel uppstod med tillägget",
"NotificationOptionPluginInstalled": "Tillägg har installerats",
@@ -113,5 +113,6 @@
"TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
- "TasksMaintenanceCategory": "Underhåll"
+ "TasksMaintenanceCategory": "Underhåll",
+ "TaskRefreshPeople": "Uppdatera Personer"
}
diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json
new file mode 100644
index 000000000..32538ac03
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/th.json
@@ -0,0 +1,71 @@
+{
+ "ProviderValue": "ผู้ให้บริการ: {0}",
+ "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว",
+ "PluginUninstalledWithName": "ถอนการติดตั้ง {0}",
+ "PluginInstalledWithName": "{0} ได้รับการติดตั้ง",
+ "Plugin": "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": "หยุดการเล่นเสียง",
+ "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 แล้ว",
+ "Latest": "ล่าสุด",
+ "LabelRunningTimeValue": "เวลาที่เล่น : {0}",
+ "LabelIpAddressValue": "IP address: {0}",
+ "ItemRemovedWithName": "{0} ถูกลบจากรายการ",
+ "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ",
+ "Inherit": "การสืบทอด",
+ "HomeVideos": "วีดีโอส่วนตัว",
+ "HeaderRecordingGroups": "ค่ายบันทึก",
+ "HeaderNextUp": "ถัดไป",
+ "HeaderLiveTV": "รายการสด",
+ "HeaderFavoriteSongs": "เพลงโปรด",
+ "HeaderFavoriteShows": "รายการโชว์โปรด",
+ "HeaderFavoriteEpisodes": "ฉากโปรด",
+ "HeaderFavoriteArtists": "นักแสดงโปรด",
+ "HeaderFavoriteAlbums": "อัมบั้มโปรด",
+ "HeaderContinueWatching": "ชมต่อจากเดิม",
+ "HeaderCameraUploads": "Upload รูปภาพ",
+ "HeaderAlbumArtists": "อัลบั้มนักแสดง",
+ "Genres": "ประเภท",
+ "Folders": "โฟลเดอร์",
+ "Favorites": "รายการโปรด",
+ "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}",
+ "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ",
+ "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ",
+ "Collections": "ชุด",
+ "ChapterNameValue": "บทที่ {0}",
+ "Channels": "ชาแนล",
+ "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}",
+ "Books": "หนังสือ",
+ "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ",
+ "Artists": "นักแสดง",
+ "Application": "แอปพลิเคชั่น",
+ "AppDeviceValues": "App: {0}, อุปกรณ์: {1}",
+ "Albums": "อัลบั้ม"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json
index 62d205516..3cf3482eb 100644
--- a/Emby.Server.Implementations/Localization/Core/tr.json
+++ b/Emby.Server.Implementations/Localization/Core/tr.json
@@ -50,7 +50,7 @@
"NotificationOptionAudioPlayback": "Ses çalma başladı",
"NotificationOptionAudioPlaybackStopped": "Ses çalma durduruldu",
"NotificationOptionCameraImageUploaded": "Kamera fotoğrafı yüklendi",
- "NotificationOptionInstallationFailed": "Yükleme başarısız oldu",
+ "NotificationOptionInstallationFailed": "Kurulum hatası",
"NotificationOptionNewLibraryContent": "Yeni içerik eklendi",
"NotificationOptionPluginError": "Eklenti hatası",
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
@@ -95,7 +95,24 @@
"VersionNumber": "Versiyon {0}",
"TaskCleanCache": "Geçici dosya klasörünü temizle",
"TasksChannelsCategory": "İnternet kanalları",
- "TasksApplicationCategory": "Yazılım",
+ "TasksApplicationCategory": "Uygulama",
"TasksLibraryCategory": "Kütüphane",
- "TasksMaintenanceCategory": "Onarım"
+ "TasksMaintenanceCategory": "Onarım",
+ "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
+ "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
+ "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
+ "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
+ "TaskRefreshChannels": "Kanalları Yenile",
+ "TaskCleanTranscodeDescription": "Bir günü dolmuş dönüştürme bilgisi içeren dosyaları siler.",
+ "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
+ "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
+ "TaskUpdatePlugins": "Eklentileri Güncelle",
+ "TaskRefreshPeople": "Kullanıcıları Yenile",
+ "TaskCleanLogsDescription": "{0} günden eski log dosyalarını siler.",
+ "TaskCleanLogs": "Log Dizinini Temizle",
+ "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve bilgileri yeniler.",
+ "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."
}
diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json
new file mode 100644
index 000000000..b2e0b66fe
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/uk.json
@@ -0,0 +1,36 @@
+{
+ "MusicVideos": "Музичні відео",
+ "Music": "Музика",
+ "Movies": "Фільми",
+ "MessageApplicationUpdatedTo": "Jellyfin Server був оновлений до версії {0}",
+ "MessageApplicationUpdated": "Jellyfin Server був оновлений",
+ "Latest": "Останні",
+ "LabelIpAddressValue": "IP-адреси: {0}",
+ "ItemRemovedWithName": "{0} видалено з бібліотеки",
+ "ItemAddedWithName": "{0} додано до бібліотеки",
+ "HeaderNextUp": "Наступний",
+ "HeaderLiveTV": "Ефірне ТБ",
+ "HeaderFavoriteSongs": "Улюблені пісні",
+ "HeaderFavoriteShows": "Улюблені шоу",
+ "HeaderFavoriteEpisodes": "Улюблені серії",
+ "HeaderFavoriteArtists": "Улюблені виконавці",
+ "HeaderFavoriteAlbums": "Улюблені альбоми",
+ "HeaderContinueWatching": "Продовжити перегляд",
+ "HeaderCameraUploads": "Завантажено з камери",
+ "HeaderAlbumArtists": "Виконавці альбомів",
+ "Genres": "Жанри",
+ "Folders": "Директорії",
+ "Favorites": "Улюблені",
+ "DeviceOnlineWithName": "{0} під'єднано",
+ "DeviceOfflineWithName": "{0} від'єднано",
+ "Collections": "Колекції",
+ "ChapterNameValue": "Глава {0}",
+ "Channels": "Канали",
+ "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}",
+ "Books": "Книги",
+ "AuthenticationSucceededWithUserName": "{0} успішно авторизовані",
+ "Artists": "Виконавці",
+ "Application": "Додаток",
+ "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
+ "Albums": "Альбоми"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json
index 224748e61..0804fc927 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-HK.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json
@@ -1,6 +1,6 @@
{
"Albums": "專輯",
- "AppDeviceValues": "軟體: {0}, 設備: {1}",
+ "AppDeviceValues": "軟件: {0}, 設備: {1}",
"Application": "應用程式",
"Artists": "藝人",
"AuthenticationSucceededWithUserName": "{0} 授權成功",
@@ -11,15 +11,15 @@
"Collections": "合輯",
"DeviceOfflineWithName": "{0} 已經斷開連結",
"DeviceOnlineWithName": "{0} 已經連接",
- "FailedLoginAttemptWithUserName": "來自 {0} 的失敗登入嘗試",
+ "FailedLoginAttemptWithUserName": "來自 {0} 的登入失敗",
"Favorites": "我的最愛",
"Folders": "檔案夾",
"Genres": "風格",
- "HeaderAlbumArtists": "專輯藝術家",
+ "HeaderAlbumArtists": "專輯藝人",
"HeaderCameraUploads": "相機上載",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛專輯",
- "HeaderFavoriteArtists": "最愛藝術家",
+ "HeaderFavoriteArtists": "最愛的藝人",
"HeaderFavoriteEpisodes": "最愛的劇集",
"HeaderFavoriteShows": "最愛的節目",
"HeaderFavoriteSongs": "最愛的歌曲",
@@ -33,14 +33,14 @@
"LabelIpAddressValue": "IP 地址: {0}",
"LabelRunningTimeValue": "運行時間: {0}",
"Latest": "最新",
- "MessageApplicationUpdated": "Jellyfin Server 已更新",
+ "MessageApplicationUpdated": "Jellyfin 伺服器已更新",
"MessageApplicationUpdatedTo": "Jellyfin 伺服器已更新至 {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 部分已更新",
+ "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已更新",
"MessageServerConfigurationUpdated": "伺服器設定已經更新",
- "MixedContent": "Mixed content",
+ "MixedContent": "混合內容",
"Movies": "電影",
"Music": "音樂",
- "MusicVideos": "音樂MV",
+ "MusicVideos": "音樂視頻",
"NameInstallFailed": "{0} 安裝失敗",
"NameSeasonNumber": "第 {0} 季",
"NameSeasonUnknown": "未知季數",
@@ -49,7 +49,7 @@
"NotificationOptionApplicationUpdateInstalled": "應用程式已更新",
"NotificationOptionAudioPlayback": "開始播放音頻",
"NotificationOptionAudioPlaybackStopped": "已停止播放音頻",
- "NotificationOptionCameraImageUploaded": "相機相片已上傳",
+ "NotificationOptionCameraImageUploaded": "相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已添加新内容",
"NotificationOptionPluginError": "擴充元件錯誤",
@@ -63,11 +63,11 @@
"NotificationOptionVideoPlaybackStopped": "已停止播放視頻",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "Plugin",
+ "Plugin": "插件",
"PluginInstalledWithName": "已安裝 {0}",
"PluginUninstalledWithName": "已移除 {0}",
"PluginUpdatedWithName": "已更新 {0}",
- "ProviderValue": "Provider: {0}",
+ "ProviderValue": "提供者: {0}",
"ScheduledTaskFailedWithName": "{0} 任務失敗",
"ScheduledTaskStartedWithName": "{0} 任務開始",
"ServerNameNeedsToBeRestarted": "{0} 需要重啓",
@@ -77,20 +77,41 @@
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
"SubtitleDownloadFailureFromForItem": "無法從 {0} 下載 {1} 的字幕",
"Sync": "同步",
- "System": "System",
+ "System": "系統",
"TvShows": "電視節目",
- "User": "User",
- "UserCreatedWithName": "用家 {0} 已創建",
- "UserDeletedWithName": "用家 {0} 已移除",
+ "User": "使用者",
+ "UserCreatedWithName": "使用者 {0} 已創建",
+ "UserDeletedWithName": "使用者 {0} 已移除",
"UserDownloadingItemWithValues": "{0} 正在下載 {1}",
- "UserLockedOutWithName": "用家 {0} 已被鎖定",
+ "UserLockedOutWithName": "使用者 {0} 已被鎖定",
"UserOfflineFromDevice": "{0} 已從 {1} 斷開",
"UserOnlineFromDevice": "{0} 已連綫,來自 {1}",
- "UserPasswordChangedWithName": "用家 {0} 的密碼已變更",
- "UserPolicyUpdatedWithName": "用戶協議已被更新為 {0}",
+ "UserPasswordChangedWithName": "使用者 {0} 的密碼已變更",
+ "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}",
"UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}",
"UserStoppedPlayingItemWithValues": "{0} 已在 {2} 上停止播放 {1}",
"ValueHasBeenAddedToLibrary": "{0} 已添加到你的媒體庫",
"ValueSpecialEpisodeName": "特典 - {0}",
- "VersionNumber": "版本{0}"
+ "VersionNumber": "版本{0}",
+ "TaskDownloadMissingSubtitles": "下載遺失的字幕",
+ "TaskUpdatePlugins": "更新插件",
+ "TasksApplicationCategory": "應用程式",
+ "TaskRefreshLibraryDescription": "掃描媒體庫以查找新文件並刷新metadata。",
+ "TasksMaintenanceCategory": "維護",
+ "TaskDownloadMissingSubtitlesDescription": "根據metadata配置在互聯網上搜索缺少的字幕。",
+ "TaskRefreshChannelsDescription": "刷新互聯網頻道信息。",
+ "TaskRefreshChannels": "刷新頻道",
+ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。",
+ "TaskCleanTranscode": "清理轉碼目錄",
+ "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+ "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。",
+ "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。",
+ "TaskCleanLogs": "清理日誌目錄",
+ "TaskRefreshLibrary": "掃描媒體庫",
+ "TaskRefreshChapterImagesDescription": "為帶有章節的視頻創建縮略圖。",
+ "TaskRefreshChapterImages": "提取章節圖像",
+ "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。",
+ "TaskCleanCache": "清理緩存目錄",
+ "TasksChannelsCategory": "互聯網頻道",
+ "TasksLibraryCategory": "庫"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json
index 21034b76f..a22f66df9 100644
--- a/Emby.Server.Implementations/Localization/Core/zh-TW.json
+++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json
@@ -20,7 +20,7 @@
"HeaderContinueWatching": "繼續觀賞",
"HeaderFavoriteAlbums": "最愛專輯",
"HeaderFavoriteArtists": "最愛演出者",
- "HeaderFavoriteEpisodes": "最愛級數",
+ "HeaderFavoriteEpisodes": "最愛影集",
"HeaderFavoriteShows": "最愛節目",
"HeaderFavoriteSongs": "最愛歌曲",
"HeaderLiveTV": "電視直播",
@@ -50,10 +50,10 @@
"NotificationOptionCameraImageUploaded": "相機相片已上傳",
"NotificationOptionInstallationFailed": "安裝失敗",
"NotificationOptionNewLibraryContent": "已新增新內容",
- "NotificationOptionPluginError": "擴充元件錯誤",
- "NotificationOptionPluginInstalled": "擴充元件已安裝",
- "NotificationOptionPluginUninstalled": "擴充元件已移除",
- "NotificationOptionPluginUpdateInstalled": "已更新擴充元件",
+ "NotificationOptionPluginError": "插件安裝錯誤",
+ "NotificationOptionPluginInstalled": "插件已安裝",
+ "NotificationOptionPluginUninstalled": "插件已移除",
+ "NotificationOptionPluginUpdateInstalled": "插件已更新",
"NotificationOptionServerRestartRequired": "伺服器需要重新啟動",
"NotificationOptionTaskFailed": "排程任務失敗",
"NotificationOptionUserLockedOut": "使用者已鎖定",
@@ -61,7 +61,7 @@
"NotificationOptionVideoPlaybackStopped": "影片停止播放",
"Photos": "相片",
"Playlists": "播放清單",
- "Plugin": "外掛",
+ "Plugin": "插件",
"PluginInstalledWithName": "{0} 已安裝",
"PluginUninstalledWithName": "{0} 已移除",
"PluginUpdatedWithName": "{0} 已更新",
@@ -91,5 +91,27 @@
"VersionNumber": "版本 {0}",
"HeaderRecordingGroups": "錄製組",
"Inherit": "繼承",
- "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕"
+ "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕",
+ "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。",
+ "TaskDownloadMissingSubtitles": "下載遺失的字幕",
+ "TaskRefreshChannels": "重新整理頻道",
+ "TaskUpdatePlugins": "更新插件",
+ "TaskRefreshPeople": "重新整理人員",
+ "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。",
+ "TaskCleanLogs": "清空紀錄資料夾",
+ "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。",
+ "TaskRefreshLibrary": "掃描媒體庫",
+ "TaskRefreshChapterImages": "擷取章節圖片",
+ "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。",
+ "TaskCleanCache": "清除快取資料夾",
+ "TasksLibraryCategory": "媒體庫",
+ "TaskRefreshChannelsDescription": "重新整理網絡頻道資料。",
+ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。",
+ "TaskCleanTranscode": "清除轉碼資料夾",
+ "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。",
+ "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的中繼資料。",
+ "TaskRefreshChapterImagesDescription": "為有章節的視頻創建縮圖。",
+ "TasksChannelsCategory": "網絡頻道",
+ "TasksApplicationCategory": "應用程式",
+ "TasksMaintenanceCategory": "維修"
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index bda43e832..e2a634e1a 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -23,9 +23,6 @@ namespace Emby.Server.Implementations.Localization
private static readonly Assembly _assembly = typeof(LocalizationManager).Assembly;
private static readonly string[] _unratedValues = { "n/a", "unrated", "not rated" };
- /// <summary>
- /// The _configuration manager.
- /// </summary>
private readonly IServerConfigurationManager _configurationManager;
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger _logger;
diff --git a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs b/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
deleted file mode 100644
index fda32da5e..000000000
--- a/Emby.Server.Implementations/Middleware/WebSocketMiddleware.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Logging;
-using WebSocketManager = Emby.Server.Implementations.WebSockets.WebSocketManager;
-
-namespace Emby.Server.Implementations.Middleware
-{
- public class WebSocketMiddleware
- {
- private readonly RequestDelegate _next;
- private readonly ILogger<WebSocketMiddleware> _logger;
- private readonly WebSocketManager _webSocketManager;
-
- public WebSocketMiddleware(RequestDelegate next, ILogger<WebSocketMiddleware> logger, WebSocketManager webSocketManager)
- {
- _next = next;
- _logger = logger;
- _webSocketManager = webSocketManager;
- }
-
- public async Task Invoke(HttpContext httpContext)
- {
- _logger.LogInformation("Handling request: " + httpContext.Request.Path);
-
- if (httpContext.WebSockets.IsWebSocketRequest)
- {
- var webSocketContext = await httpContext.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
- if (webSocketContext != null)
- {
- await _webSocketManager.OnWebSocketConnected(webSocketContext).ConfigureAwait(false);
- }
- }
- else
- {
- await _next.Invoke(httpContext).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/Emby.Server.Implementations/Net/IWebSocket.cs b/Emby.Server.Implementations/Net/IWebSocket.cs
deleted file mode 100644
index 4d160aa66..000000000
--- a/Emby.Server.Implementations/Net/IWebSocket.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Emby.Server.Implementations.Net
-{
- /// <summary>
- /// Interface IWebSocket
- /// </summary>
- public interface IWebSocket : IDisposable
- {
- /// <summary>
- /// Occurs when [closed].
- /// </summary>
- event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets or sets the state.
- /// </summary>
- /// <value>The state.</value>
- WebSocketState State { get; }
-
- /// <summary>
- /// Gets or sets the receive action.
- /// </summary>
- /// <value>The receive action.</value>
- Action<byte[]> OnReceiveBytes { get; set; }
-
- /// <summary>
- /// Sends the async.
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the asynchronous.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken);
- }
-}
diff --git a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs b/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
deleted file mode 100644
index 6880766f9..000000000
--- a/Emby.Server.Implementations/Net/WebSocketConnectEventArgs.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using Microsoft.AspNetCore.Http;
-
-namespace Emby.Server.Implementations.Net
-{
- public class WebSocketConnectEventArgs : EventArgs
- {
- /// <summary>
- /// Gets or sets the URL.
- /// </summary>
- /// <value>The URL.</value>
- public string Url { get; set; }
- /// <summary>
- /// Gets or sets the query string.
- /// </summary>
- /// <value>The query string.</value>
- public IQueryCollection QueryString { get; set; }
- /// <summary>
- /// Gets or sets the web socket.
- /// </summary>
- /// <value>The web socket.</value>
- public IWebSocket WebSocket { get; set; }
- /// <summary>
- /// Gets or sets the endpoint.
- /// </summary>
- /// <value>The endpoint.</value>
- public string Endpoint { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
index ecf58dbc0..6ffa581a9 100644
--- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -32,22 +32,8 @@ namespace Emby.Server.Implementations.ScheduledTasks
private readonly ConcurrentQueue<Tuple<Type, TaskOptions>> _taskQueue =
new ConcurrentQueue<Tuple<Type, TaskOptions>>();
- /// <summary>
- /// Gets or sets the json serializer.
- /// </summary>
- /// <value>The json serializer.</value>
private readonly IJsonSerializer _jsonSerializer;
-
- /// <summary>
- /// Gets or sets the application paths.
- /// </summary>
- /// <value>The application paths.</value>
private readonly IApplicationPaths _applicationPaths;
-
- /// <summary>
- /// Gets the logger.
- /// </summary>
- /// <value>The logger.</value>
private readonly ILogger _logger;
private readonly IFileSystem _fileSystem;
@@ -56,17 +42,17 @@ namespace Emby.Server.Implementations.ScheduledTasks
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="jsonSerializer">The json serializer.</param>
- /// <param name="loggerFactory">The logger factory.</param>
+ /// <param name="logger">The logger.</param>
/// <param name="fileSystem">The filesystem manager.</param>
public TaskManager(
IApplicationPaths applicationPaths,
IJsonSerializer jsonSerializer,
- ILoggerFactory loggerFactory,
+ ILogger<TaskManager> logger,
IFileSystem fileSystem)
{
_applicationPaths = applicationPaths;
_jsonSerializer = jsonSerializer;
- _logger = loggerFactory.CreateLogger(nameof(TaskManager));
+ _logger = logger;
_fileSystem = fileSystem;
ScheduledTasks = Array.Empty<IScheduledTaskWorker>();
diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
index 1ef5c4b99..4e4029f06 100644
--- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs
+++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs
@@ -15,8 +15,8 @@ namespace Emby.Server.Implementations.Security
{
public class AuthenticationRepository : BaseSqliteRepository, IAuthenticationRepository
{
- public AuthenticationRepository(ILoggerFactory loggerFactory, IServerConfigurationManager config)
- : base(loggerFactory.CreateLogger(nameof(AuthenticationRepository)))
+ public AuthenticationRepository(ILogger<AuthenticationRepository> logger, IServerConfigurationManager config)
+ : base(logger)
{
DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "authentication.db");
}
diff --git a/Emby.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Implementations/ServerApplicationPaths.cs
index 2f57c97a1..dfdd4200e 100644
--- a/Emby.Server.Implementations/ServerApplicationPaths.cs
+++ b/Emby.Server.Implementations/ServerApplicationPaths.cs
@@ -9,8 +9,6 @@ namespace Emby.Server.Implementations
/// </summary>
public class ServerApplicationPaths : BaseApplicationPaths, IServerApplicationPaths
{
- private string _internalMetadataPath;
-
/// <summary>
/// Initializes a new instance of the <see cref="ServerApplicationPaths" /> class.
/// </summary>
@@ -27,6 +25,7 @@ namespace Emby.Server.Implementations
cacheDirectoryPath,
webDirectoryPath)
{
+ InternalMetadataPath = DefaultInternalMetadataPath;
}
/// <summary>
@@ -98,12 +97,11 @@ namespace Emby.Server.Implementations
/// <value>The user configuration directory path.</value>
public string UserConfigurationDirectoryPath => Path.Combine(ConfigurationDirectoryPath, "users");
+ /// <inheritdoc/>
+ public string DefaultInternalMetadataPath => Path.Combine(ProgramDataPath, "metadata");
+
/// <inheritdoc />
- public string InternalMetadataPath
- {
- get => _internalMetadataPath ?? (_internalMetadataPath = Path.Combine(DataPath, "metadata"));
- set => _internalMetadataPath = value;
- }
+ public string InternalMetadataPath { get; set; }
/// <inheritdoc />
public string VirtualInternalMetadataPath { get; } = "%MetadataPath%";
diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
index 23e22afd5..56e23d549 100644
--- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
+++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Reflection;
+using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
@@ -81,7 +82,7 @@ namespace Emby.Server.Implementations.Services
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 = LeftPart(propertyTextValue, ',');
+ propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString();
}
var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue);
@@ -95,19 +96,6 @@ namespace Emby.Server.Implementations.Services
return instance;
}
-
- public static string LeftPart(string strVal, char needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle);
- return pos == -1
- ? strVal
- : strVal.Substring(0, pos);
- }
}
internal static class TypeAccessor
diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs
index 5d4407f3b..483c63ade 100644
--- a/Emby.Server.Implementations/Services/UrlExtensions.cs
+++ b/Emby.Server.Implementations/Services/UrlExtensions.cs
@@ -1,4 +1,5 @@
using System;
+using MediaBrowser.Common.Extensions;
namespace Emby.Server.Implementations.Services
{
@@ -13,25 +14,12 @@ namespace Emby.Server.Implementations.Services
public static string GetMethodName(this Type type)
{
var typeName = type.FullName != null // can be null, e.g. generic types
- ? LeftPart(type.FullName, "[[") // Generic Fullname
- .Replace(type.Namespace + ".", string.Empty) // Trim Namespaces
- .Replace("+", ".") // Convert nested into normal type
+ ? 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;
}
-
- private static string LeftPart(string strVal, string needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle, StringComparison.OrdinalIgnoreCase);
- return pos == -1
- ? strVal
- : strVal.Substring(0, pos);
- }
}
}
diff --git a/Emby.Server.Implementations/Session/HttpSessionController.cs b/Emby.Server.Implementations/Session/HttpSessionController.cs
deleted file mode 100644
index dfb81816c..000000000
--- a/Emby.Server.Implementations/Session/HttpSessionController.cs
+++ /dev/null
@@ -1,191 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Net;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Common.Net;
-using MediaBrowser.Controller.Session;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Session;
-
-namespace Emby.Server.Implementations.Session
-{
- public class HttpSessionController : ISessionController
- {
- private readonly IHttpClient _httpClient;
- private readonly IJsonSerializer _json;
- private readonly ISessionManager _sessionManager;
-
- public SessionInfo Session { get; private set; }
-
- private readonly string _postUrl;
-
- public HttpSessionController(IHttpClient httpClient,
- IJsonSerializer json,
- SessionInfo session,
- string postUrl, ISessionManager sessionManager)
- {
- _httpClient = httpClient;
- _json = json;
- Session = session;
- _postUrl = postUrl;
- _sessionManager = sessionManager;
- }
-
- private string PostUrl => string.Format("http://{0}{1}", Session.RemoteEndPoint, _postUrl);
-
- public bool IsSessionActive => (DateTime.UtcNow - Session.LastActivityDate).TotalMinutes <= 5;
-
- public bool SupportsMediaControl => true;
-
- private Task SendMessage(string name, string messageId, CancellationToken cancellationToken)
- {
- return SendMessage(name, messageId, new Dictionary<string, string>(), cancellationToken);
- }
-
- private Task SendMessage(string name, string messageId, Dictionary<string, string> args, CancellationToken cancellationToken)
- {
- args["messageId"] = messageId;
- var url = PostUrl + "/" + name + ToQueryString(args);
-
- return SendRequest(new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = false
- });
- }
-
- private Task SendPlayCommand(PlayRequest command, string messageId, CancellationToken cancellationToken)
- {
- var dict = new Dictionary<string, string>();
-
- dict["ItemIds"] = string.Join(",", command.ItemIds.Select(i => i.ToString("N", CultureInfo.InvariantCulture)).ToArray());
-
- if (command.StartPositionTicks.HasValue)
- {
- dict["StartPositionTicks"] = command.StartPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.AudioStreamIndex.HasValue)
- {
- dict["AudioStreamIndex"] = command.AudioStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.SubtitleStreamIndex.HasValue)
- {
- dict["SubtitleStreamIndex"] = command.SubtitleStreamIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (command.StartIndex.HasValue)
- {
- dict["StartIndex"] = command.StartIndex.Value.ToString(CultureInfo.InvariantCulture);
- }
- if (!string.IsNullOrEmpty(command.MediaSourceId))
- {
- dict["MediaSourceId"] = command.MediaSourceId;
- }
-
- return SendMessage(command.PlayCommand.ToString(), messageId, dict, cancellationToken);
- }
-
- private Task SendPlaystateCommand(PlaystateRequest command, string messageId, CancellationToken cancellationToken)
- {
- var args = new Dictionary<string, string>();
-
- if (command.Command == PlaystateCommand.Seek)
- {
- if (!command.SeekPositionTicks.HasValue)
- {
- throw new ArgumentException("SeekPositionTicks cannot be null");
- }
-
- args["SeekPositionTicks"] = command.SeekPositionTicks.Value.ToString(CultureInfo.InvariantCulture);
- }
-
- return SendMessage(command.Command.ToString(), messageId, args, cancellationToken);
- }
-
- private string[] _supportedMessages = Array.Empty<string>();
- public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
- {
- if (!IsSessionActive)
- {
- return Task.CompletedTask;
- }
-
- if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
- {
- return SendPlayCommand(data as PlayRequest, messageId, cancellationToken);
- }
- if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
- {
- return SendPlaystateCommand(data as PlaystateRequest, messageId, cancellationToken);
- }
- if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
- {
- var command = data as GeneralCommand;
- return SendMessage(command.Name, messageId, command.Arguments, cancellationToken);
- }
-
- if (!_supportedMessages.Contains(name, StringComparer.OrdinalIgnoreCase))
- {
- return Task.CompletedTask;
- }
-
- var url = PostUrl + "/" + name;
-
- url += "?messageId=" + messageId;
-
- var options = new HttpRequestOptions
- {
- Url = url,
- CancellationToken = cancellationToken,
- BufferContent = false
- };
-
- if (data != null)
- {
- if (typeof(T) == typeof(string))
- {
- var str = data as string;
- if (!string.IsNullOrEmpty(str))
- {
- options.RequestContent = str;
- options.RequestContentType = "application/json";
- }
- }
- else
- {
- options.RequestContent = _json.SerializeToString(data);
- options.RequestContentType = "application/json";
- }
- }
-
- return SendRequest(options);
- }
-
- private async Task SendRequest(HttpRequestOptions options)
- {
- using (var response = await _httpClient.Post(options).ConfigureAwait(false))
- {
-
- }
- }
-
- private static string ToQueryString(Dictionary<string, string> nvc)
- {
- var array = (from item in nvc
- select string.Format("{0}={1}", WebUtility.UrlEncode(item.Key), WebUtility.UrlEncode(item.Value)))
- .ToArray();
-
- var args = string.Join("&", array);
-
- if (string.IsNullOrEmpty(args))
- {
- return args;
- }
-
- return "?" + args;
- }
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index 2c8b2f29d..611c73a2f 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -25,6 +25,7 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
@@ -477,8 +478,7 @@ namespace Emby.Server.Implementations.Session
Client = appName,
DeviceId = deviceId,
ApplicationVersion = appVersion,
- Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture),
- ServerId = _appHost.SystemId
+ Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
var username = user?.Name;
@@ -1042,12 +1042,12 @@ namespace Emby.Server.Implementations.Session
private static async Task SendMessageToSession<T>(SessionInfo session, string name, T data, CancellationToken cancellationToken)
{
- var controllers = session.SessionControllers.ToArray();
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var controllers = session.SessionControllers;
+ var messageId = Guid.NewGuid();
foreach (var controller in controllers)
{
- await controller.SendMessage(name, messageId, data, controllers, cancellationToken).ConfigureAwait(false);
+ await controller.SendMessage(name, messageId, data, cancellationToken).ConfigureAwait(false);
}
}
@@ -1055,13 +1055,13 @@ namespace Emby.Server.Implementations.Session
{
IEnumerable<Task> GetTasks()
{
- var messageId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
+ var messageId = Guid.NewGuid();
foreach (var session in sessions)
{
var controllers = session.SessionControllers;
foreach (var controller in controllers)
{
- yield return controller.SendMessage(name, messageId, data, controllers, cancellationToken);
+ yield return controller.SendMessage(name, messageId, data, cancellationToken);
}
}
}
@@ -1154,6 +1154,22 @@ namespace Emby.Server.Implementations.Session
await SendMessageToSession(session, "Play", command, cancellationToken).ConfigureAwait(false);
}
+ /// <inheritdoc />
+ public async Task SendSyncPlayCommand(string sessionId, SendCommand command, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ var session = GetSessionToRemoteControl(sessionId);
+ await SendMessageToSession(session, "SyncPlayCommand", command, cancellationToken).ConfigureAwait(false);
+ }
+
+ /// <inheritdoc />
+ public async Task SendSyncPlayGroupUpdate<T>(string sessionId, GroupUpdate<T> command, CancellationToken cancellationToken)
+ {
+ CheckDisposed();
+ var session = GetSessionToRemoteControl(sessionId);
+ await SendMessageToSession(session, "SyncPlayGroupUpdate", command, cancellationToken).ConfigureAwait(false);
+ }
+
private IEnumerable<BaseItem> TranslateItemForPlayback(Guid id, User user)
{
var item = _libraryManager.GetItemById(id);
@@ -1432,7 +1448,7 @@ namespace Emby.Server.Implementations.Session
if (user == null)
{
AuthenticationFailed?.Invoke(this, new GenericEventArgs<AuthenticationRequest>(request));
- throw new SecurityException("Invalid username or password entered.");
+ throw new AuthenticationException("Invalid username or password entered.");
}
if (!string.IsNullOrEmpty(request.DeviceId)
@@ -1780,7 +1796,7 @@ namespace Emby.Server.Implementations.Session
throw new ArgumentNullException(nameof(info));
}
- var user = info.UserId.Equals(Guid.Empty)
+ var user = info.UserId == Guid.Empty
? null
: _userManager.GetUserById(info.UserId);
diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
index 930f2d35d..e7b4b0ec3 100644
--- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
+++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs
@@ -1,9 +1,13 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
-using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@@ -12,9 +16,24 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionWebSocketListener
/// </summary>
- public class SessionWebSocketListener : IWebSocketListener, IDisposable
+ public sealed class SessionWebSocketListener : IWebSocketListener, IDisposable
{
/// <summary>
+ /// The timeout in seconds after which a WebSocket is considered to be lost.
+ /// </summary>
+ public const int WebSocketLostTimeout = 60;
+
+ /// <summary>
+ /// The keep-alive interval factor; controls how often the watcher will check on the status of the WebSockets.
+ /// </summary>
+ public const float IntervalFactor = 0.2f;
+
+ /// <summary>
+ /// The ForceKeepAlive factor; controls when a ForceKeepAlive is sent.
+ /// </summary>
+ public const float ForceKeepAliveFactor = 0.75f;
+
+ /// <summary>
/// The _session manager
/// </summary>
private readonly ISessionManager _sessionManager;
@@ -23,42 +42,62 @@ namespace Emby.Server.Implementations.Session
/// The _logger
/// </summary>
private readonly ILogger _logger;
+ private readonly ILoggerFactory _loggerFactory;
+
+ private readonly IHttpServer _httpServer;
/// <summary>
- /// The _dto service
+ /// The KeepAlive cancellation token.
/// </summary>
- private readonly IJsonSerializer _json;
+ private CancellationTokenSource _keepAliveCancellationToken;
- private readonly IHttpServer _httpServer;
+ /// <summary>
+ /// Lock used for accesing the KeepAlive cancellation token.
+ /// </summary>
+ private readonly object _keepAliveLock = new object();
+
+ /// <summary>
+ /// The WebSocket watchlist.
+ /// </summary>
+ private readonly HashSet<IWebSocketConnection> _webSockets = new HashSet<IWebSocketConnection>();
+ /// <summary>
+ /// Lock used for accesing the WebSockets watchlist.
+ /// </summary>
+ private readonly object _webSocketsLock = new object();
/// <summary>
/// Initializes a new instance of the <see cref="SessionWebSocketListener" /> class.
/// </summary>
+ /// <param name="logger">The logger.</param>
/// <param name="sessionManager">The session manager.</param>
/// <param name="loggerFactory">The logger factory.</param>
- /// <param name="json">The json.</param>
/// <param name="httpServer">The HTTP server.</param>
- public SessionWebSocketListener(ISessionManager sessionManager, ILoggerFactory loggerFactory, IJsonSerializer json, IHttpServer httpServer)
+ public SessionWebSocketListener(
+ ILogger<SessionWebSocketListener> logger,
+ ISessionManager sessionManager,
+ ILoggerFactory loggerFactory,
+ IHttpServer httpServer)
{
+ _logger = logger;
_sessionManager = sessionManager;
- _logger = loggerFactory.CreateLogger(GetType().Name);
- _json = json;
+ _loggerFactory = loggerFactory;
_httpServer = httpServer;
- httpServer.WebSocketConnected += _serverManager_WebSocketConnected;
+
+ httpServer.WebSocketConnected += OnServerManagerWebSocketConnected;
}
- void _serverManager_WebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
+ private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e)
{
- var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint);
-
+ var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString());
if (session != null)
{
EnsureController(session, e.Argument);
+ await KeepAliveWebSocket(e.Argument);
}
else
{
- _logger.LogWarning("Unable to determine session based on url: {0}", e.Argument.Url);
+ _logger.LogWarning("Unable to determine session based on query string: {0}", e.Argument.QueryString);
}
}
@@ -79,9 +118,11 @@ namespace Emby.Server.Implementations.Session
return _sessionManager.GetSessionByAuthenticationToken(token, deviceId, remoteEndpoint);
}
+ /// <inheritdoc />
public void Dispose()
{
- _httpServer.WebSocketConnected -= _serverManager_WebSocketConnected;
+ _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected;
+ StopKeepAlive();
}
/// <summary>
@@ -94,10 +135,212 @@ namespace Emby.Server.Implementations.Session
private void EnsureController(SessionInfo session, IWebSocketConnection connection)
{
- var controllerInfo = session.EnsureController<WebSocketController>(s => new WebSocketController(s, _logger, _sessionManager));
+ var controllerInfo = session.EnsureController<WebSocketController>(
+ s => new WebSocketController(_loggerFactory.CreateLogger<WebSocketController>(), s, _sessionManager));
var controller = (WebSocketController)controllerInfo.Item1;
controller.AddWebSocket(connection);
}
+
+ /// <summary>
+ /// Called when a WebSocket is closed.
+ /// </summary>
+ /// <param name="sender">The WebSocket.</param>
+ /// <param name="e">The event arguments.</param>
+ private void OnWebSocketClosed(object sender, EventArgs e)
+ {
+ var webSocket = (IWebSocketConnection)sender;
+ _logger.LogDebug("WebSocket {0} is closed.", webSocket);
+ RemoveWebSocket(webSocket);
+ }
+
+ /// <summary>
+ /// Adds a WebSocket to the KeepAlive watchlist.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket to monitor.</param>
+ private async Task KeepAliveWebSocket(IWebSocketConnection webSocket)
+ {
+ lock (_webSocketsLock)
+ {
+ if (!_webSockets.Add(webSocket))
+ {
+ _logger.LogWarning("Multiple attempts to keep alive single WebSocket {0}", webSocket);
+ return;
+ }
+ webSocket.Closed += OnWebSocketClosed;
+ webSocket.LastKeepAliveDate = DateTime.UtcNow;
+
+ StartKeepAlive();
+ }
+
+ // Notify WebSocket about timeout
+ try
+ {
+ await SendForceKeepAlive(webSocket);
+ }
+ catch (WebSocketException exception)
+ {
+ _logger.LogWarning(exception, "Cannot send ForceKeepAlive message to WebSocket {0}.", webSocket);
+ }
+ }
+
+ /// <summary>
+ /// Removes a WebSocket from the KeepAlive watchlist.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket to remove.</param>
+ private void RemoveWebSocket(IWebSocketConnection webSocket)
+ {
+ lock (_webSocketsLock)
+ {
+ if (!_webSockets.Remove(webSocket))
+ {
+ _logger.LogWarning("WebSocket {0} not on watchlist.", webSocket);
+ }
+ else
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Starts the KeepAlive watcher.
+ /// </summary>
+ private void StartKeepAlive()
+ {
+ lock (_keepAliveLock)
+ {
+ if (_keepAliveCancellationToken == null)
+ {
+ _keepAliveCancellationToken = new CancellationTokenSource();
+ // Start KeepAlive watcher
+ _ = RepeatAsyncCallbackEvery(
+ KeepAliveSockets,
+ TimeSpan.FromSeconds(WebSocketLostTimeout * IntervalFactor),
+ _keepAliveCancellationToken.Token);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Stops the KeepAlive watcher.
+ /// </summary>
+ private void StopKeepAlive()
+ {
+ lock (_keepAliveLock)
+ {
+ if (_keepAliveCancellationToken != null)
+ {
+ _keepAliveCancellationToken.Cancel();
+ _keepAliveCancellationToken = null;
+ }
+ }
+
+ lock (_webSocketsLock)
+ {
+ foreach (var webSocket in _webSockets)
+ {
+ webSocket.Closed -= OnWebSocketClosed;
+ }
+
+ _webSockets.Clear();
+ }
+ }
+
+ /// <summary>
+ /// Checks status of KeepAlive of WebSockets.
+ /// </summary>
+ private async Task KeepAliveSockets()
+ {
+ List<IWebSocketConnection> inactive;
+ List<IWebSocketConnection> lost;
+
+ lock (_webSocketsLock)
+ {
+ _logger.LogDebug("Watching {0} WebSockets.", _webSockets.Count);
+
+ inactive = _webSockets.Where(i =>
+ {
+ var elapsed = (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds;
+ return (elapsed > WebSocketLostTimeout * ForceKeepAliveFactor) && (elapsed < WebSocketLostTimeout);
+ }).ToList();
+ lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList();
+ }
+
+ if (inactive.Any())
+ {
+ _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count);
+ }
+
+ foreach (var webSocket in inactive)
+ {
+ try
+ {
+ await SendForceKeepAlive(webSocket);
+ }
+ catch (WebSocketException exception)
+ {
+ _logger.LogInformation(exception, "Error sending ForceKeepAlive message to WebSocket.");
+ lost.Add(webSocket);
+ }
+ }
+
+ lock (_webSocketsLock)
+ {
+ if (lost.Any())
+ {
+ _logger.LogInformation("Lost {0} WebSockets.", lost.Count);
+ foreach (var webSocket in lost)
+ {
+ // TODO: handle session relative to the lost webSocket
+ RemoveWebSocket(webSocket);
+ }
+ }
+
+ if (!_webSockets.Any())
+ {
+ StopKeepAlive();
+ }
+ }
+ }
+
+ /// <summary>
+ /// Sends a ForceKeepAlive message to a WebSocket.
+ /// </summary>
+ /// <param name="webSocket">The WebSocket.</param>
+ /// <returns>Task.</returns>
+ private Task SendForceKeepAlive(IWebSocketConnection webSocket)
+ {
+ return webSocket.SendAsync(new WebSocketMessage<int>
+ {
+ MessageType = "ForceKeepAlive",
+ Data = WebSocketLostTimeout
+ }, CancellationToken.None);
+ }
+
+ /// <summary>
+ /// Runs a given async callback once every specified interval time, until cancelled.
+ /// </summary>
+ /// <param name="callback">The async callback.</param>
+ /// <param name="interval">The interval time.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <returns>Task.</returns>
+ private async Task RepeatAsyncCallbackEvery(Func<Task> callback, TimeSpan interval, CancellationToken cancellationToken)
+ {
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ await callback();
+ Task task = Task.Delay(interval, cancellationToken);
+
+ try
+ {
+ await task;
+ }
+ catch (TaskCanceledException)
+ {
+ return;
+ }
+ }
+ }
}
}
diff --git a/Emby.Server.Implementations/Session/WebSocketController.cs b/Emby.Server.Implementations/Session/WebSocketController.cs
index 0d483c55f..a0274acd2 100644
--- a/Emby.Server.Implementations/Session/WebSocketController.cs
+++ b/Emby.Server.Implementations/Session/WebSocketController.cs
@@ -1,3 +1,7 @@
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+#nullable enable
+
using System;
using System.Collections.Generic;
using System.Linq;
@@ -11,60 +15,63 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Session
{
- public class WebSocketController : ISessionController, IDisposable
+ public sealed class WebSocketController : ISessionController, IDisposable
{
- public SessionInfo Session { get; private set; }
- public IReadOnlyList<IWebSocketConnection> Sockets { get; private set; }
-
private readonly ILogger _logger;
-
private readonly ISessionManager _sessionManager;
+ private readonly SessionInfo _session;
- public WebSocketController(SessionInfo session, ILogger logger, ISessionManager sessionManager)
+ private readonly List<IWebSocketConnection> _sockets;
+ private bool _disposed = false;
+
+ public WebSocketController(
+ ILogger<WebSocketController> logger,
+ SessionInfo session,
+ ISessionManager sessionManager)
{
- Session = session;
_logger = logger;
+ _session = session;
_sessionManager = sessionManager;
- Sockets = new List<IWebSocketConnection>();
+ _sockets = new List<IWebSocketConnection>();
}
private bool HasOpenSockets => GetActiveSockets().Any();
+ /// <inheritdoc />
public bool SupportsMediaControl => HasOpenSockets;
+ /// <inheritdoc />
public bool IsSessionActive => HasOpenSockets;
private IEnumerable<IWebSocketConnection> GetActiveSockets()
- {
- return Sockets
- .OrderByDescending(i => i.LastActivityDate)
- .Where(i => i.State == WebSocketState.Open);
- }
+ => _sockets.Where(i => i.State == WebSocketState.Open);
public void AddWebSocket(IWebSocketConnection connection)
{
- var sockets = Sockets.ToList();
- sockets.Add(connection);
+ _logger.LogDebug("Adding websocket to session {Session}", _session.Id);
+ _sockets.Add(connection);
- Sockets = sockets;
-
- connection.Closed += connection_Closed;
+ connection.Closed += OnConnectionClosed;
}
- void connection_Closed(object sender, EventArgs e)
+ private void OnConnectionClosed(object sender, EventArgs e)
{
var connection = (IWebSocketConnection)sender;
- var sockets = Sockets.ToList();
- sockets.Remove(connection);
-
- Sockets = sockets;
-
- _sessionManager.CloseIfNeeded(Session);
+ _logger.LogDebug("Removing websocket from session {Session}", _session.Id);
+ _sockets.Remove(connection);
+ connection.Closed -= OnConnectionClosed;
+ _sessionManager.CloseIfNeeded(_session);
}
- public Task SendMessage<T>(string name, string messageId, T data, ISessionController[] allControllers, CancellationToken cancellationToken)
+ /// <inheritdoc />
+ public Task SendMessage<T>(
+ string name,
+ Guid messageId,
+ T data,
+ CancellationToken cancellationToken)
{
var socket = GetActiveSockets()
+ .OrderByDescending(i => i.LastActivityDate)
.FirstOrDefault();
if (socket == null)
@@ -72,21 +79,30 @@ namespace Emby.Server.Implementations.Session
return Task.CompletedTask;
}
- return socket.SendAsync(new WebSocketMessage<T>
- {
- Data = data,
- MessageType = name,
- MessageId = messageId
-
- }, cancellationToken);
+ return socket.SendAsync(
+ new WebSocketMessage<T>
+ {
+ Data = data,
+ MessageType = name,
+ MessageId = messageId
+ },
+ cancellationToken);
}
+ /// <inheritdoc />
public void Dispose()
{
- foreach (var socket in Sockets.ToList())
+ if (_disposed)
{
- socket.Closed -= connection_Closed;
+ return;
}
+
+ foreach (var socket in _sockets)
+ {
+ socket.Closed -= OnConnectionClosed;
+ }
+
+ _disposed = true;
}
}
}
diff --git a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
deleted file mode 100644
index 67521d6c6..000000000
--- a/Emby.Server.Implementations/SocketSharp/SharpWebSocket.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using System;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Net;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class SharpWebSocket : IWebSocket
- {
- /// <summary>
- /// The logger
- /// </summary>
- private readonly ILogger _logger;
-
- public event EventHandler<EventArgs> Closed;
-
- /// <summary>
- /// Gets or sets the web socket.
- /// </summary>
- /// <value>The web socket.</value>
- private readonly WebSocket _webSocket;
-
- private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
- private bool _disposed;
-
- public SharpWebSocket(WebSocket socket, ILogger logger)
- {
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- _webSocket = socket ?? throw new ArgumentNullException(nameof(socket));
- }
-
- /// <summary>
- /// Gets the state.
- /// </summary>
- /// <value>The state.</value>
- public WebSocketState State => _webSocket.State;
-
- /// <summary>
- /// Sends the async.
- /// </summary>
- /// <param name="bytes">The bytes.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken)
- {
- return _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Binary, endOfMessage, cancellationToken);
- }
-
- /// <summary>
- /// Sends the asynchronous.
- /// </summary>
- /// <param name="text">The text.</param>
- /// <param name="endOfMessage">if set to <c>true</c> [end of message].</param>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken)
- {
- return _webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(text)), WebSocketMessageType.Text, endOfMessage, cancellationToken);
- }
-
- /// <summary>
- /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- /// </summary>
- 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 (_disposed)
- {
- return;
- }
-
- if (dispose)
- {
- _cancellationTokenSource.Cancel();
- if (_webSocket.State == WebSocketState.Open)
- {
- _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed by client",
- CancellationToken.None);
- }
- Closed?.Invoke(this, EventArgs.Empty);
- }
-
- _disposed = true;
- }
-
- /// <summary>
- /// Gets or sets the receive action.
- /// </summary>
- /// <value>The receive action.</value>
- public Action<byte[]> OnReceiveBytes { get; set; }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
deleted file mode 100644
index b85750c9b..000000000
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpListener.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.HttpServer;
-using Emby.Server.Implementations.Net;
-using MediaBrowser.Model.Services;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.Extensions.Logging;
-using Microsoft.Net.Http.Headers;
-
-namespace Emby.Server.Implementations.SocketSharp
-{
- public class WebSocketSharpListener : IHttpListener
- {
- private readonly ILogger _logger;
-
- private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
- private CancellationToken _disposeCancellationToken;
-
- public WebSocketSharpListener(ILogger<WebSocketSharpListener> logger)
- {
- _logger = logger;
- _disposeCancellationToken = _disposeCancellationTokenSource.Token;
- }
-
- public Func<Exception, IRequest, bool, bool, Task> ErrorHandler { get; set; }
-
- public Func<IHttpRequest, string, string, string, CancellationToken, Task> RequestHandler { get; set; }
-
- public Action<WebSocketConnectEventArgs> WebSocketConnected { get; set; }
-
- private static void LogRequest(ILogger logger, HttpRequest request)
- {
- var url = request.GetDisplayUrl();
-
- logger.LogInformation("WS {Url}. UserAgent: {UserAgent}", url, request.Headers[HeaderNames.UserAgent].ToString());
- }
-
- public async Task ProcessWebSocketRequest(HttpContext ctx)
- {
- try
- {
- LogRequest(_logger, ctx.Request);
- var endpoint = ctx.Connection.RemoteIpAddress.ToString();
- var url = ctx.Request.GetDisplayUrl();
-
- var webSocketContext = await ctx.WebSockets.AcceptWebSocketAsync(null).ConfigureAwait(false);
- var socket = new SharpWebSocket(webSocketContext, _logger);
-
- WebSocketConnected(new WebSocketConnectEventArgs
- {
- Url = url,
- QueryString = ctx.Request.Query,
- WebSocket = socket,
- Endpoint = endpoint
- });
-
- WebSocketReceiveResult result;
- var message = new List<byte>();
-
- do
- {
- var buffer = WebSocket.CreateServerBuffer(4096);
- result = await webSocketContext.ReceiveAsync(buffer, _disposeCancellationToken);
- message.AddRange(buffer.Array.Take(result.Count));
-
- if (result.EndOfMessage)
- {
- socket.OnReceiveBytes(message.ToArray());
- message.Clear();
- }
- } while (socket.State == WebSocketState.Open && result.MessageType != WebSocketMessageType.Close);
-
-
- if (webSocketContext.State == WebSocketState.Open)
- {
- await webSocketContext.CloseAsync(
- result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
- result.CloseStatusDescription,
- _disposeCancellationToken).ConfigureAwait(false);
- }
-
- socket.Dispose();
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "AcceptWebSocketAsync error");
- if (!ctx.Response.HasStarted)
- {
- ctx.Response.StatusCode = 500;
- }
- }
- }
-
- public Task Stop()
- {
- _disposeCancellationTokenSource.Cancel();
- return Task.CompletedTask;
- }
-
- /// <summary>
- /// Releases the unmanaged resources and disposes of the managed resources used.
- /// </summary>
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- private bool _disposed;
-
- /// <summary>
- /// Releases the unmanaged resources and disposes of the managed resources used.
- /// </summary>
- /// <param name="disposing">Whether or not the managed resources should be disposed.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- Stop().GetAwaiter().GetResult();
- }
-
- _disposed = true;
- }
- }
-}
diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
index 1781df8b5..ee5131c1f 100644
--- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
+++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs
@@ -3,6 +3,7 @@ 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;
@@ -62,6 +63,9 @@ namespace Emby.Server.Implementations.SocketSharp
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;
}
}
@@ -89,7 +93,10 @@ namespace Emby.Server.Implementations.SocketSharp
public IQueryCollection QueryString => Request.Query;
- public bool IsLocal => Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress);
+ 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;
@@ -216,14 +223,14 @@ namespace Emby.Server.Implementations.SocketSharp
pi = pi.Slice(1);
}
- format = LeftPart(pi, '/');
+ format = pi.LeftPart('/');
if (format.Length > FormatMaxLength)
{
return null;
}
}
- format = LeftPart(format, '.');
+ format = format.LeftPart('.');
if (format.Contains("json", StringComparison.OrdinalIgnoreCase))
{
return "application/json";
@@ -235,16 +242,5 @@ namespace Emby.Server.Implementations.SocketSharp
return null;
}
-
- public static ReadOnlySpan<char> LeftPart(ReadOnlySpan<char> strVal, char needle)
- {
- if (strVal == null)
- {
- return null;
- }
-
- var pos = strVal.IndexOf(needle);
- return pos == -1 ? strVal : strVal.Slice(0, pos);
- }
}
}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
new file mode 100644
index 000000000..d430d4d16
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs
@@ -0,0 +1,517 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.Session;
+using MediaBrowser.Model.SyncPlay;
+
+namespace Emby.Server.Implementations.SyncPlay
+{
+ /// <summary>
+ /// Class SyncPlayController.
+ /// </summary>
+ /// <remarks>
+ /// Class is not thread-safe, external locking is required when accessing methods.
+ /// </remarks>
+ public class SyncPlayController : ISyncPlayController
+ {
+ /// <summary>
+ /// Used to filter the sessions of a group.
+ /// </summary>
+ private enum BroadcastType
+ {
+ /// <summary>
+ /// All sessions will receive the message.
+ /// </summary>
+ AllGroup = 0,
+ /// <summary>
+ /// Only the specified session will receive the message.
+ /// </summary>
+ CurrentSession = 1,
+ /// <summary>
+ /// All sessions, except the current one, will receive the message.
+ /// </summary>
+ AllExceptCurrentSession = 2,
+ /// <summary>
+ /// Only sessions that are not buffering will receive the message.
+ /// </summary>
+ AllReady = 3
+ }
+
+ /// <summary>
+ /// The session manager.
+ /// </summary>
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// The SyncPlay manager.
+ /// </summary>
+ private readonly ISyncPlayManager _syncPlayManager;
+
+ /// <summary>
+ /// The group to manage.
+ /// </summary>
+ private readonly GroupInfo _group = new GroupInfo();
+
+ /// <inheritdoc />
+ public Guid GetGroupId() => _group.GroupId;
+
+ /// <inheritdoc />
+ public Guid GetPlayingItemId() => _group.PlayingItem.Id;
+
+ /// <inheritdoc />
+ public bool IsGroupEmpty() => _group.IsEmpty();
+
+ public SyncPlayController(
+ ISessionManager sessionManager,
+ ISyncPlayManager syncPlayManager)
+ {
+ _sessionManager = sessionManager;
+ _syncPlayManager = syncPlayManager;
+ }
+
+ /// <summary>
+ /// Converts DateTime to UTC string.
+ /// </summary>
+ /// <param name="date">The date to convert.</param>
+ /// <value>The UTC string.</value>
+ private string DateToUTCString(DateTime date)
+ {
+ return date.ToUniversalTime().ToString("o");
+ }
+
+ /// <summary>
+ /// Filters sessions of this group.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <value>The array of sessions matching the filter.</value>
+ private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type)
+ {
+ switch (type)
+ {
+ case BroadcastType.CurrentSession:
+ return new SessionInfo[] { from };
+ case BroadcastType.AllGroup:
+ return _group.Participants.Values.Select(
+ session => session.Session
+ ).ToArray();
+ case BroadcastType.AllExceptCurrentSession:
+ return _group.Participants.Values.Select(
+ session => session.Session
+ ).Where(
+ session => !session.Id.Equals(from.Id)
+ ).ToArray();
+ case BroadcastType.AllReady:
+ return _group.Participants.Values.Where(
+ session => !session.IsBuffering
+ ).Select(
+ session => session.Session
+ ).ToArray();
+ default:
+ return Array.Empty<SessionInfo>();
+ }
+ }
+
+ /// <summary>
+ /// Sends a GroupUpdate message to the interested sessions.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The task.</value>
+ private Task SendGroupUpdate<T>(SessionInfo from, BroadcastType type, GroupUpdate<T> message, CancellationToken cancellationToken)
+ {
+ IEnumerable<Task> GetTasks()
+ {
+ SessionInfo[] sessions = FilterSessions(from, type);
+ foreach (var session in sessions)
+ {
+ yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ /// <summary>
+ /// Sends a playback command to the interested sessions.
+ /// </summary>
+ /// <param name="from">The current session.</param>
+ /// <param name="type">The filtering type.</param>
+ /// <param name="message">The message to send.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <value>The task.</value>
+ private Task SendCommand(SessionInfo from, BroadcastType type, SendCommand message, CancellationToken cancellationToken)
+ {
+ IEnumerable<Task> GetTasks()
+ {
+ SessionInfo[] sessions = FilterSessions(from, type);
+ foreach (var session in sessions)
+ {
+ yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken);
+ }
+ }
+
+ return Task.WhenAll(GetTasks());
+ }
+
+ /// <summary>
+ /// Builds a new playback command with some default values.
+ /// </summary>
+ /// <param name="type">The command type.</param>
+ /// <value>The SendCommand.</value>
+ private SendCommand NewSyncPlayCommand(SendCommandType type)
+ {
+ return new SendCommand()
+ {
+ GroupId = _group.GroupId.ToString(),
+ Command = type,
+ PositionTicks = _group.PositionTicks,
+ When = DateToUTCString(_group.LastActivity),
+ EmittedAt = DateToUTCString(DateTime.UtcNow)
+ };
+ }
+
+ /// <summary>
+ /// Builds a new group update message.
+ /// </summary>
+ /// <param name="type">The update type.</param>
+ /// <param name="data">The data to send.</param>
+ /// <value>The GroupUpdate.</value>
+ private GroupUpdate<T> NewSyncPlayGroupUpdate<T>(GroupUpdateType type, T data)
+ {
+ return new GroupUpdate<T>()
+ {
+ GroupId = _group.GroupId.ToString(),
+ Type = type,
+ Data = data
+ };
+ }
+
+ /// <inheritdoc />
+ public void InitGroup(SessionInfo session, CancellationToken cancellationToken)
+ {
+ _group.AddSession(session);
+ _syncPlayManager.AddSessionToGroup(session, this);
+
+ _group.PlayingItem = session.FullNowPlayingItem;
+ _group.IsPaused = true;
+ _group.PositionTicks = session.PlayState.PositionTicks ?? 0;
+ _group.LastActivity = DateTime.UtcNow;
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+ var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public void SessionJoin(SessionInfo session, JoinGroupRequest request, CancellationToken cancellationToken)
+ {
+ if (session.NowPlayingItem?.Id == _group.PlayingItem.Id && request.PlayingItemId == _group.PlayingItem.Id)
+ {
+ _group.AddSession(session);
+ _syncPlayManager.AddSessionToGroup(session, this);
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupJoined, DateToUTCString(DateTime.UtcNow));
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserJoined, session.UserName);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+
+ // Client join and play, syncing will happen client side
+ if (!_group.IsPaused)
+ {
+ var playCommand = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.CurrentSession, playCommand, cancellationToken);
+ }
+ else
+ {
+ var pauseCommand = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, pauseCommand, cancellationToken);
+ }
+ }
+ else
+ {
+ var playRequest = new PlayRequest();
+ playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id };
+ playRequest.StartPositionTicks = _group.PositionTicks;
+ var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public void SessionLeave(SessionInfo session, CancellationToken cancellationToken)
+ {
+ _group.RemoveSession(session);
+ _syncPlayManager.RemoveSessionFromGroup(session, this);
+
+ var updateSession = NewSyncPlayGroupUpdate(GroupUpdateType.GroupLeft, _group.PositionTicks);
+ SendGroupUpdate(session, BroadcastType.CurrentSession, updateSession, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.UserLeft, session.UserName);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+ }
+
+ /// <inheritdoc />
+ public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ // The server's job is to mantain a consistent state to which clients refer to,
+ // as also to notify clients of state changes.
+ // The actual syncing of media playback happens client side.
+ // Clients are aware of the server's time and use it to sync.
+ switch (request.Type)
+ {
+ case PlaybackRequestType.Play:
+ HandlePlayRequest(session, request, cancellationToken);
+ break;
+ case PlaybackRequestType.Pause:
+ HandlePauseRequest(session, request, cancellationToken);
+ break;
+ case PlaybackRequestType.Seek:
+ HandleSeekRequest(session, request, cancellationToken);
+ break;
+ case PlaybackRequestType.Buffering:
+ HandleBufferingRequest(session, request, cancellationToken);
+ break;
+ case PlaybackRequestType.BufferingDone:
+ HandleBufferingDoneRequest(session, request, cancellationToken);
+ break;
+ case PlaybackRequestType.UpdatePing:
+ HandlePingUpdateRequest(session, request);
+ break;
+ }
+ }
+
+ /// <summary>
+ /// Handles a play action requested by a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The play action.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private void HandlePlayRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ if (_group.IsPaused)
+ {
+ // Pick a suitable time that accounts for latency
+ var delay = _group.GetHighestPing() * 2;
+ delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+ // Unpause group and set starting point in future
+ // Clients will start playback at LastActivity (datetime) from PositionTicks (playback position)
+ // The added delay does not guarantee, of course, that the command will be received in time
+ // Playback synchronization will mainly happen client side
+ _group.IsPaused = false;
+ _group.LastActivity = DateTime.UtcNow.AddMilliseconds(
+ delay
+ );
+
+ var command = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Handles a pause action requested by a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The pause action.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private void HandlePauseRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ if (!_group.IsPaused)
+ {
+ // Pause group and compute the media playback position
+ _group.IsPaused = true;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - _group.LastActivity;
+ _group.LastActivity = currentTime;
+ // Seek only if playback actually started
+ // (a pause request may be issued during the delay added to account for latency)
+ _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ var command = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Handles a seek action requested by a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The seek action.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private void HandleSeekRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ // Sanitize PositionTicks
+ var ticks = SanitizePositionTicks(request.PositionTicks);
+
+ // Pause and seek
+ _group.IsPaused = true;
+ _group.PositionTicks = ticks;
+ _group.LastActivity = DateTime.UtcNow;
+
+ var command = NewSyncPlayCommand(SendCommandType.Seek);
+ SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+ }
+
+ /// <summary>
+ /// Handles a buffering action requested by a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The buffering action.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private void HandleBufferingRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ if (!_group.IsPaused)
+ {
+ // Pause group and compute the media playback position
+ _group.IsPaused = true;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - _group.LastActivity;
+ _group.LastActivity = currentTime;
+ _group.PositionTicks += elapsedTime.Ticks > 0 ? elapsedTime.Ticks : 0;
+
+ _group.SetBuffering(session, true);
+
+ // Send pause command to all non-buffering sessions
+ var command = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.AllReady, command, cancellationToken);
+
+ var updateOthers = NewSyncPlayGroupUpdate(GroupUpdateType.GroupWait, session.UserName);
+ SendGroupUpdate(session, BroadcastType.AllExceptCurrentSession, updateOthers, cancellationToken);
+ }
+ else
+ {
+ // Client got lost, sending current state
+ var command = NewSyncPlayCommand(SendCommandType.Pause);
+ SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Handles a buffering-done action requested by a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The buffering-done action.</param>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ private void HandleBufferingDoneRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ if (_group.IsPaused)
+ {
+ _group.SetBuffering(session, false);
+
+ var requestTicks = SanitizePositionTicks(request.PositionTicks);
+
+ var when = request.When ?? DateTime.UtcNow;
+ var currentTime = DateTime.UtcNow;
+ var elapsedTime = currentTime - when;
+ var clientPosition = TimeSpan.FromTicks(requestTicks) + elapsedTime;
+ var delay = _group.PositionTicks - clientPosition.Ticks;
+
+ if (_group.IsBuffering())
+ {
+ // Others are still buffering, tell this client to pause when ready
+ var command = NewSyncPlayCommand(SendCommandType.Pause);
+ var pauseAtTime = currentTime.AddMilliseconds(delay);
+ command.When = DateToUTCString(pauseAtTime);
+ SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+ }
+ else
+ {
+ // Let other clients resume as soon as the buffering client catches up
+ _group.IsPaused = false;
+
+ if (delay > _group.GetHighestPing() * 2)
+ {
+ // Client that was buffering is recovering, notifying others to resume
+ _group.LastActivity = currentTime.AddMilliseconds(
+ delay
+ );
+ var command = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.AllExceptCurrentSession, command, cancellationToken);
+ }
+ else
+ {
+ // Client, that was buffering, resumed playback but did not update others in time
+ delay = _group.GetHighestPing() * 2;
+ delay = delay < _group.DefaulPing ? _group.DefaulPing : delay;
+
+ _group.LastActivity = currentTime.AddMilliseconds(
+ delay
+ );
+
+ var command = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.AllGroup, command, cancellationToken);
+ }
+ }
+ }
+ else
+ {
+ // Group was not waiting, make sure client has latest state
+ var command = NewSyncPlayCommand(SendCommandType.Play);
+ SendCommand(session, BroadcastType.CurrentSession, command, cancellationToken);
+ }
+ }
+
+ /// <summary>
+ /// Sanitizes the PositionTicks, considers the current playing item when available.
+ /// </summary>
+ /// <param name="positionTicks">The PositionTicks.</param>
+ /// <value>The sanitized PositionTicks.</value>
+ private long SanitizePositionTicks(long? positionTicks)
+ {
+ var ticks = positionTicks ?? 0;
+ ticks = ticks >= 0 ? ticks : 0;
+ if (_group.PlayingItem != null)
+ {
+ var runTimeTicks = _group.PlayingItem.RunTimeTicks ?? 0;
+ ticks = ticks > runTimeTicks ? runTimeTicks : ticks;
+ }
+
+ return ticks;
+ }
+
+ /// <summary>
+ /// Updates ping of a session.
+ /// </summary>
+ /// <param name="session">The session.</param>
+ /// <param name="request">The update.</param>
+ 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.DefaulPing);
+ }
+
+ /// <inheritdoc />
+ public GroupInfoView GetInfo()
+ {
+ return new GroupInfoView()
+ {
+ GroupId = GetGroupId().ToString(),
+ PlayingItemName = _group.PlayingItem.Name,
+ PlayingItemId = _group.PlayingItem.Id.ToString(),
+ PositionTicks = _group.PositionTicks,
+ Participants = _group.Participants.Values.Select(session => session.Session.UserName).Distinct().ToList()
+ };
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
new file mode 100644
index 000000000..1f76dd4e3
--- /dev/null
+++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs
@@ -0,0 +1,398 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Logging;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Session;
+using MediaBrowser.Controller.SyncPlay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.SyncPlay;
+
+namespace Emby.Server.Implementations.SyncPlay
+{
+ /// <summary>
+ /// Class SyncPlayManager.
+ /// </summary>
+ public class SyncPlayManager : ISyncPlayManager, IDisposable
+ {
+ /// <summary>
+ /// The logger.
+ /// </summary>
+ private readonly ILogger _logger;
+
+ /// <summary>
+ /// The user manager.
+ /// </summary>
+ private readonly IUserManager _userManager;
+
+ /// <summary>
+ /// The session manager.
+ /// </summary>
+ private readonly ISessionManager _sessionManager;
+
+ /// <summary>
+ /// The library manager.
+ /// </summary>
+ private readonly ILibraryManager _libraryManager;
+
+ /// <summary>
+ /// The map between sessions and groups.
+ /// </summary>
+ private readonly Dictionary<string, ISyncPlayController> _sessionToGroupMap =
+ new Dictionary<string, ISyncPlayController>(StringComparer.OrdinalIgnoreCase);
+
+ /// <summary>
+ /// The groups.
+ /// </summary>
+ private readonly Dictionary<Guid, ISyncPlayController> _groups =
+ new Dictionary<Guid, ISyncPlayController>();
+
+ /// <summary>
+ /// Lock used for accesing any group.
+ /// </summary>
+ private readonly object _groupsLock = new object();
+
+ private bool _disposed = false;
+
+ public SyncPlayManager(
+ ILogger<SyncPlayManager> logger,
+ IUserManager userManager,
+ ISessionManager sessionManager,
+ ILibraryManager libraryManager)
+ {
+ _logger = logger;
+ _userManager = userManager;
+ _sessionManager = sessionManager;
+ _libraryManager = libraryManager;
+
+ _sessionManager.SessionEnded += OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStopped += OnSessionManagerPlaybackStopped;
+ }
+
+ /// <summary>
+ /// Gets all groups.
+ /// </summary>
+ /// <value>All groups.</value>
+ public IEnumerable<ISyncPlayController> Groups => _groups.Values;
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ /// <summary>
+ /// Releases unmanaged and optionally managed resources.
+ /// </summary>
+ /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _sessionManager.SessionEnded -= OnSessionManagerSessionEnded;
+ _sessionManager.PlaybackStopped -= OnSessionManagerPlaybackStopped;
+
+ _disposed = true;
+ }
+
+ private void CheckDisposed()
+ {
+ if (_disposed)
+ {
+ throw new ObjectDisposedException(GetType().Name);
+ }
+ }
+
+ private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
+ {
+ var session = e.SessionInfo;
+ if (!IsSessionInGroup(session))
+ {
+ return;
+ }
+
+ LeaveGroup(session, CancellationToken.None);
+ }
+
+ private void OnSessionManagerPlaybackStopped(object sender, PlaybackStopEventArgs e)
+ {
+ var session = e.Session;
+ if (!IsSessionInGroup(session))
+ {
+ return;
+ }
+
+ LeaveGroup(session, CancellationToken.None);
+ }
+
+ private bool IsSessionInGroup(SessionInfo session)
+ {
+ return _sessionToGroupMap.ContainsKey(session.Id);
+ }
+
+ private bool HasAccessToItem(User user, Guid itemId)
+ {
+ var item = _libraryManager.GetItemById(itemId);
+
+ // Check ParentalRating access
+ var hasParentalRatingAccess = true;
+ if (user.Policy.MaxParentalRating.HasValue)
+ {
+ hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
+ }
+
+ if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
+ {
+ var collections = _libraryManager.GetCollectionFolders(item).Select(
+ folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
+ );
+ var intersect = collections.Intersect(user.Policy.EnabledFolders);
+ return intersect.Any();
+ }
+ else
+ {
+ return hasParentalRatingAccess;
+ }
+ }
+
+ private Guid? GetSessionGroup(SessionInfo session)
+ {
+ ISyncPlayController group;
+ _sessionToGroupMap.TryGetValue(session.Id, out group);
+ if (group != null)
+ {
+ return group.GetGroupId();
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+ /// <inheritdoc />
+ public void NewGroup(SessionInfo session, CancellationToken cancellationToken)
+ {
+ var user = _userManager.GetUserById(session.UserId);
+
+ if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
+ {
+ _logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.CreateGroupDenied
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ lock (_groupsLock)
+ {
+ if (IsSessionInGroup(session))
+ {
+ LeaveGroup(session, cancellationToken);
+ }
+
+ var group = new SyncPlayController(_sessionManager, this);
+ _groups[group.GetGroupId()] = group;
+
+ group.InitGroup(session, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public void JoinGroup(SessionInfo session, Guid groupId, JoinGroupRequest request, CancellationToken cancellationToken)
+ {
+ var user = _userManager.GetUserById(session.UserId);
+
+ if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ {
+ _logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.JoinGroupDenied
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ lock (_groupsLock)
+ {
+ ISyncPlayController group;
+ _groups.TryGetValue(groupId, out group);
+
+ if (group == null)
+ {
+ _logger.LogWarning("JoinGroup: {0} tried to join group {0} that does not exist.", session.Id, groupId);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.GroupDoesNotExist
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ if (!HasAccessToItem(user, group.GetPlayingItemId()))
+ {
+ _logger.LogWarning("JoinGroup: {0} does not have access to {1}.", session.Id, group.GetPlayingItemId());
+
+ var error = new GroupUpdate<string>()
+ {
+ GroupId = group.GetGroupId().ToString(),
+ Type = GroupUpdateType.LibraryAccessDenied
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ if (IsSessionInGroup(session))
+ {
+ if (GetSessionGroup(session).Equals(groupId))
+ {
+ return;
+ }
+
+ LeaveGroup(session, cancellationToken);
+ }
+
+ group.SessionJoin(session, request, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public void LeaveGroup(SessionInfo session, CancellationToken cancellationToken)
+ {
+ // TODO: determine what happens to users that are in a group and get their permissions revoked
+ lock (_groupsLock)
+ {
+ ISyncPlayController group;
+ _sessionToGroupMap.TryGetValue(session.Id, out group);
+
+ if (group == null)
+ {
+ _logger.LogWarning("LeaveGroup: {0} does not belong to any group.", session.Id);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.NotInGroup
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ group.SessionLeave(session, cancellationToken);
+
+ if (group.IsGroupEmpty())
+ {
+ _logger.LogInformation("LeaveGroup: removing empty group {0}.", group.GetGroupId());
+ _groups.Remove(group.GetGroupId(), out _);
+ }
+ }
+ }
+
+ /// <inheritdoc />
+ public List<GroupInfoView> ListGroups(SessionInfo session, Guid filterItemId)
+ {
+ var user = _userManager.GetUserById(session.UserId);
+
+ if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ {
+ return new List<GroupInfoView>();
+ }
+
+ // Filter by item if requested
+ if (!filterItemId.Equals(Guid.Empty))
+ {
+ return _groups.Values.Where(
+ group => group.GetPlayingItemId().Equals(filterItemId) && HasAccessToItem(user, group.GetPlayingItemId())
+ ).Select(
+ group => group.GetInfo()
+ ).ToList();
+ }
+ // Otherwise show all available groups
+ else
+ {
+ return _groups.Values.Where(
+ group => HasAccessToItem(user, group.GetPlayingItemId())
+ ).Select(
+ group => group.GetInfo()
+ ).ToList();
+ }
+ }
+
+ /// <inheritdoc />
+ public void HandleRequest(SessionInfo session, PlaybackRequest request, CancellationToken cancellationToken)
+ {
+ var user = _userManager.GetUserById(session.UserId);
+
+ if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
+ {
+ _logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.JoinGroupDenied
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ lock (_groupsLock)
+ {
+ ISyncPlayController group;
+ _sessionToGroupMap.TryGetValue(session.Id, out group);
+
+ if (group == null)
+ {
+ _logger.LogWarning("HandleRequest: {0} does not belong to any group.", session.Id);
+
+ var error = new GroupUpdate<string>()
+ {
+ Type = GroupUpdateType.NotInGroup
+ };
+ _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
+ return;
+ }
+
+ group.HandleRequest(session, request, cancellationToken);
+ }
+ }
+
+ /// <inheritdoc />
+ public void AddSessionToGroup(SessionInfo session, ISyncPlayController group)
+ {
+ if (IsSessionInGroup(session))
+ {
+ throw new InvalidOperationException("Session in other group already!");
+ }
+
+ _sessionToGroupMap[session.Id] = group;
+ }
+
+ /// <inheritdoc />
+ public void RemoveSessionFromGroup(SessionInfo session, ISyncPlayController group)
+ {
+ if (!IsSessionInGroup(session))
+ {
+ throw new InvalidOperationException("Session not in any group!");
+ }
+
+ ISyncPlayController tempGroup;
+ _sessionToGroupMap.Remove(session.Id, out tempGroup);
+
+ if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
+ {
+ throw new InvalidOperationException("Session was in wrong group!");
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index c91d137a7..a26f714b1 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller;
using MediaBrowser.Model.ApiClient;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Udp
@@ -21,6 +22,12 @@ namespace Emby.Server.Implementations.Udp
/// </summary>
private readonly ILogger _logger;
private readonly IServerApplicationHost _appHost;
+ private readonly IConfiguration _config;
+
+ /// <summary>
+ /// Address Override Configuration Key.
+ /// </summary>
+ public const string AddressOverrideConfigKey = "PublishedServerUrl";
private Socket _udpSocket;
private IPEndPoint _endpoint;
@@ -31,15 +38,18 @@ namespace Emby.Server.Implementations.Udp
/// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class.
/// </summary>
- public UdpServer(ILogger logger, IServerApplicationHost appHost)
+ public UdpServer(ILogger logger, IServerApplicationHost appHost, IConfiguration configuration)
{
_logger = logger;
_appHost = appHost;
+ _config = configuration;
}
private async Task RespondToV2Message(string messageText, EndPoint endpoint, CancellationToken cancellationToken)
{
- var localUrl = await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
+ string localUrl = !string.IsNullOrEmpty(_config[AddressOverrideConfigKey])
+ ? _config[AddressOverrideConfigKey]
+ : await _appHost.GetLocalApiUrl(cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(localUrl))
{
@@ -105,7 +115,7 @@ namespace Emby.Server.Implementations.Udp
}
catch (SocketException ex)
{
- _logger.LogError(ex, "Failed to receive data drom socket");
+ _logger.LogError(ex, "Failed to receive data from socket");
}
catch (OperationCanceledException)
{
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 25f70471a..0b2309889 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -26,7 +26,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.Updates
{
/// <summary>
- /// Manages all install, uninstall and update operations (both plugins and system).
+ /// Manages all install, uninstall, and update operations for the system and individual plugins.
/// </summary>
public class InstallationManager : IInstallationManager
{
@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Updates
public const string PluginManifestUrlKey = "InstallationManager:PluginManifestUrl";
/// <summary>
- /// The _logger.
+ /// The logger.
/// </summary>
private readonly ILogger _logger;
private readonly IApplicationPaths _appPaths;
@@ -112,10 +112,10 @@ namespace Emby.Server.Implementations.Updates
public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled;
/// <inheritdoc />
- public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated;
+ public event EventHandler<GenericEventArgs<(IPlugin, VersionInfo)>> PluginUpdated;
/// <inheritdoc />
- public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled;
+ public event EventHandler<GenericEventArgs<VersionInfo>> PluginInstalled;
/// <inheritdoc />
public IEnumerable<InstallationInfo> CompletedInstallations => _completedInstallationsInternal;
@@ -183,61 +183,56 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
- IEnumerable<PackageVersionInfo> availableVersions,
- Version minVersion = null,
- PackageVersionClass classification = PackageVersionClass.Release)
+ public IEnumerable<VersionInfo> GetCompatibleVersions(
+ IEnumerable<VersionInfo> availableVersions,
+ Version minVersion = null)
{
var appVer = _applicationHost.ApplicationVersion;
availableVersions = availableVersions
- .Where(x => x.classification == classification
- && Version.Parse(x.requiredVersionStr) <= appVer);
+ .Where(x => Version.Parse(x.targetAbi) <= appVer);
if (minVersion != null)
{
- availableVersions = availableVersions.Where(x => x.Version >= minVersion);
+ availableVersions = availableVersions.Where(x => x.version >= minVersion);
}
- return availableVersions.OrderByDescending(x => x.Version);
+ return availableVersions.OrderByDescending(x => x.version);
}
/// <inheritdoc />
- public IEnumerable<PackageVersionInfo> GetCompatibleVersions(
+ public IEnumerable<VersionInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string name = null,
Guid guid = default,
- Version minVersion = null,
- PackageVersionClass classification = PackageVersionClass.Release)
+ Version minVersion = null)
{
var package = FilterPackages(availablePackages, name, guid).FirstOrDefault();
- // Package not found.
+ // Package not found in repository
if (package == null)
{
- return Enumerable.Empty<PackageVersionInfo>();
+ return Enumerable.Empty<VersionInfo>();
}
return GetCompatibleVersions(
package.versions,
- minVersion,
- classification);
+ minVersion);
}
/// <inheritdoc />
- public async Task<IEnumerable<PackageVersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
+ public async Task<IEnumerable<VersionInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default)
{
var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false);
return GetAvailablePluginUpdates(catalog);
}
- private IEnumerable<PackageVersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
+ private IEnumerable<VersionInfo> GetAvailablePluginUpdates(IReadOnlyList<PackageInfo> pluginCatalog)
{
foreach (var plugin in _applicationHost.Plugins)
{
- var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version, _applicationHost.SystemUpdateLevel);
- var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version);
- if (version != null
- && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase)))
+ var compatibleversions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, plugin.Version);
+ var version = compatibleversions.FirstOrDefault(y => y.version > plugin.Version);
+ if (version != null && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase)))
{
yield return version;
}
@@ -245,7 +240,7 @@ namespace Emby.Server.Implementations.Updates
}
/// <inheritdoc />
- public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken)
+ public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken)
{
if (package == null)
{
@@ -254,11 +249,9 @@ namespace Emby.Server.Implementations.Updates
var installationInfo = new InstallationInfo
{
- Id = Guid.NewGuid(),
+ Guid = package.guid,
Name = package.name,
- AssemblyGuid = package.guid,
- UpdateClass = package.classification,
- Version = package.versionStr
+ Version = package.version.ToString()
};
var innerCancellationTokenSource = new CancellationTokenSource();
@@ -276,7 +269,7 @@ namespace Emby.Server.Implementations.Updates
var installationEventArgs = new InstallationEventArgs
{
InstallationInfo = installationInfo,
- PackageVersionInfo = package
+ VersionInfo = package
};
PackageInstalling?.Invoke(this, installationEventArgs);
@@ -301,7 +294,7 @@ namespace Emby.Server.Implementations.Updates
_currentInstallations.Remove(tuple);
}
- _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr);
+ _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version);
PackageInstallationCancelled?.Invoke(this, installationEventArgs);
@@ -337,7 +330,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(PackageVersionInfo package, CancellationToken cancellationToken)
+ private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken)
{
// Set last update time if we were installed before
IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase))
@@ -349,26 +342,26 @@ namespace Emby.Server.Implementations.Updates
// Do plugin-specific processing
if (plugin == null)
{
- _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
+ _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version);
- PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package));
+ PluginInstalled?.Invoke(this, new GenericEventArgs<VersionInfo>(package));
}
else
{
- _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification);
+ _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version);
- PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package)));
+ PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package)));
}
_applicationHost.NotifyPendingRestart();
}
- private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken)
+ private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken)
{
- var extension = Path.GetExtension(package.targetFilename);
+ var extension = Path.GetExtension(package.filename);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{
- _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename);
+ _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename);
return;
}
@@ -415,7 +408,7 @@ namespace Emby.Server.Implementations.Updates
}
/// <summary>
- /// Uninstalls a plugin
+ /// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
public void UninstallPlugin(IPlugin plugin)
@@ -473,7 +466,7 @@ namespace Emby.Server.Implementations.Updates
{
lock (_currentInstallationsLock)
{
- var install = _currentInstallations.Find(x => x.info.Id == id);
+ var install = _currentInstallations.Find(x => x.info.Guid == id.ToString());
if (install == default((InstallationInfo, CancellationTokenSource)))
{
return false;
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs b/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
deleted file mode 100644
index eb1877440..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketHandler.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-
-namespace Emby.Server.Implementations.WebSockets
-{
- public interface IWebSocketHandler
- {
- Task ProcessMessage(WebSocketMessage<object> message, TaskCompletionSource<bool> taskCompletionSource);
- }
-}
diff --git a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs b/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
deleted file mode 100644
index 31a7468fb..000000000
--- a/Emby.Server.Implementations/WebSockets/WebSocketManager.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Net.WebSockets;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Model.Net;
-using MediaBrowser.Model.Serialization;
-using Microsoft.Extensions.Logging;
-using UtfUnknown;
-
-namespace Emby.Server.Implementations.WebSockets
-{
- public class WebSocketManager
- {
- private readonly IWebSocketHandler[] _webSocketHandlers;
- private readonly IJsonSerializer _jsonSerializer;
- private readonly ILogger<WebSocketManager> _logger;
- private const int BufferSize = 4096;
-
- public WebSocketManager(IWebSocketHandler[] webSocketHandlers, IJsonSerializer jsonSerializer, ILogger<WebSocketManager> logger)
- {
- _webSocketHandlers = webSocketHandlers;
- _jsonSerializer = jsonSerializer;
- _logger = logger;
- }
-
- public async Task OnWebSocketConnected(WebSocket webSocket)
- {
- var taskCompletionSource = new TaskCompletionSource<bool>();
- var cancellationToken = new CancellationTokenSource().Token;
- WebSocketReceiveResult result;
- var message = new List<byte>();
-
- // Keep listening for incoming messages, otherwise the socket closes automatically
- do
- {
- var buffer = WebSocket.CreateServerBuffer(BufferSize);
- result = await webSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false);
- message.AddRange(buffer.Array.Take(result.Count));
-
- if (result.EndOfMessage)
- {
- await ProcessMessage(message.ToArray(), taskCompletionSource).ConfigureAwait(false);
- message.Clear();
- }
- } while (!taskCompletionSource.Task.IsCompleted &&
- webSocket.State == WebSocketState.Open &&
- result.MessageType != WebSocketMessageType.Close);
-
- if (webSocket.State == WebSocketState.Open)
- {
- await webSocket.CloseAsync(
- result.CloseStatus ?? WebSocketCloseStatus.NormalClosure,
- result.CloseStatusDescription,
- cancellationToken).ConfigureAwait(false);
- }
- }
-
- private async Task ProcessMessage(byte[] messageBytes, TaskCompletionSource<bool> taskCompletionSource)
- {
- var charset = CharsetDetector.DetectFromBytes(messageBytes).Detected?.EncodingName;
- var message = string.Equals(charset, "utf-8", StringComparison.OrdinalIgnoreCase)
- ? Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length)
- : Encoding.ASCII.GetString(messageBytes, 0, messageBytes.Length);
-
- // All messages are expected to be valid JSON objects
- if (!message.StartsWith("{", StringComparison.OrdinalIgnoreCase))
- {
- _logger.LogDebug("Received web socket message that is not a json structure: {Message}", message);
- return;
- }
-
- try
- {
- var info = _jsonSerializer.DeserializeFromString<WebSocketMessage<object>>(message);
-
- _logger.LogDebug("Websocket message received: {0}", info.MessageType);
-
- var tasks = _webSocketHandlers.Select(handler => Task.Run(() =>
- {
- try
- {
- handler.ProcessMessage(info, taskCompletionSource).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "{HandlerType} failed processing WebSocket message {MessageType}",
- handler.GetType().Name, info.MessageType ?? string.Empty);
- }
- }));
-
- await Task.WhenAll(tasks);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error processing web socket message");
- }
- }
- }
-}