diff options
Diffstat (limited to 'Emby.Server.Implementations')
85 files changed, 2023 insertions, 609 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index e6410f857..be4e05a64 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -45,6 +45,7 @@ using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; 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; @@ -78,6 +79,7 @@ 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.Configuration; @@ -613,6 +615,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); + serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); + serviceCollection.AddSingleton<LiveTvDtoService>(); serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 138832fb8..04fe0bacb 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -46,14 +46,14 @@ namespace Emby.Server.Implementations.Channels 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="logger">The logger.</param> /// <param name="config">The server configuration manager.</param> /// <param name="fileSystem">The filesystem.</param> /// <param name="userDataManager">The user data manager.</param> diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs index 305e67e8c..94ee1ced7 100644 --- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs @@ -90,7 +90,7 @@ namespace Emby.Server.Implementations.Configuration ValidateMetadataPath(newConfig); ValidateSslCertificate(newConfig); - ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration> { Argument = newConfig }); + ConfigurationUpdating?.Invoke(this, new GenericEventArgs<ServerConfiguration>(newConfig)); base.ReplaceConfiguration(newConfiguration); } diff --git a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs index a037415a9..fd302d136 100644 --- a/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs +++ b/Emby.Server.Implementations/Cryptography/CryptographyProvider.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; using System.Security.Cryptography; @@ -129,8 +131,6 @@ namespace Emby.Server.Implementations.Cryptography _randomNumberGenerator.Dispose(); } - _randomNumberGenerator = null; - _disposed = true; } } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index ca5cd6fdd..18d235c87 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -33,7 +35,7 @@ using SQLitePCL.pretty; namespace Emby.Server.Implementations.Data { /// <summary> - /// Class SQLiteItemRepository + /// Class SQLiteItemRepository. /// </summary> public class SqliteItemRepository : BaseSqliteRepository, IItemRepository { @@ -1141,24 +1143,24 @@ namespace Emby.Server.Implementations.Data public string ToValueString(ItemImageInfo image) { - var delimeter = "*"; - - var path = image.Path; + const string Delimeter = "*"; - if (path == null) - { - path = string.Empty; - } + var path = image.Path ?? string.Empty; + var hash = image.BlurHash ?? string.Empty; return GetPathToSave(path) + - delimeter + + Delimeter + image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + - delimeter + + Delimeter + image.Type + - delimeter + + Delimeter + image.Width.ToString(CultureInfo.InvariantCulture) + - delimeter + - image.Height.ToString(CultureInfo.InvariantCulture); + Delimeter + + image.Height.ToString(CultureInfo.InvariantCulture) + + Delimeter + + // Replace delimiters with other characters. + // This can be removed when we migrate to a proper DB. + hash.Replace('*', '/').Replace('|', '\\'); } public ItemImageInfo ItemImageInfoFromValueString(string value) @@ -1192,6 +1194,11 @@ namespace Emby.Server.Implementations.Data image.Width = width; image.Height = height; } + + if (parts.Length >= 6) + { + image.BlurHash = parts[5].Replace('/', '*').Replace('\\', '|'); + } } return image; @@ -1971,6 +1978,7 @@ namespace Emby.Server.Implementations.Data /// Gets the chapter. /// </summary> /// <param name="reader">The reader.</param> + /// <param name="item">The item.</param> /// <returns>ChapterInfo.</returns> private ChapterInfo GetChapter(IReadOnlyList<IResultSetValue> reader, BaseItem item) { diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index 2283f2433..e39e0aa78 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -62,13 +62,7 @@ namespace Emby.Server.Implementations.Devices { _authRepo.UpdateDeviceOptions(deviceId, options); - if (DeviceOptionsUpdated != null) - { - DeviceOptionsUpdated(this, new GenericEventArgs<Tuple<string, DeviceOptions>>() - { - Argument = new Tuple<string, DeviceOptions>(deviceId, options) - }); - } + DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, options))); } public DeviceOptions GetDeviceOptions(string deviceId) diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index c4b65d265..38c4f940d 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -605,7 +605,7 @@ namespace Emby.Server.Implementations.Dto if (dictionary.TryGetValue(person.Name, out Person entity)) { - baseItemPerson.PrimaryImageTag = GetImageCacheTag(entity, ImageType.Primary); + baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture); list.Add(baseItemPerson); } @@ -654,6 +654,70 @@ namespace Emby.Server.Implementations.Dto return _libraryManager.GetGenreId(name); } + private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0) + { + var image = item.GetImageInfo(imageType, imageIndex); + if (image != null) + { + return GetTagAndFillBlurhash(dto, item, image); + } + + return null; + } + + private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image) + { + var tag = GetImageCacheTag(item, image); + if (!string.IsNullOrEmpty(image.BlurHash)) + { + if (dto.ImageBlurHashes == null) + { + dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); + } + + if (!dto.ImageBlurHashes.ContainsKey(image.Type)) + { + dto.ImageBlurHashes[image.Type] = new Dictionary<string, string>(); + } + + dto.ImageBlurHashes[image.Type][tag] = image.BlurHash; + } + + return tag; + } + + private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, int limit) + { + return GetTagsAndFillBlurhashes(dto, item, imageType, item.GetImages(imageType).Take(limit).ToList()); + } + + private string[] GetTagsAndFillBlurhashes(BaseItemDto dto, BaseItem item, ImageType imageType, List<ItemImageInfo> images) + { + var tags = GetImageTags(item, images); + var hashes = new Dictionary<string, string>(); + for (int i = 0; i < images.Count; i++) + { + var img = images[i]; + if (!string.IsNullOrEmpty(img.BlurHash)) + { + var tag = tags[i]; + hashes[tag] = img.BlurHash; + } + } + + if (hashes.Count > 0) + { + if (dto.ImageBlurHashes == null) + { + dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); + } + + dto.ImageBlurHashes[imageType] = hashes; + } + + return tags; + } + /// <summary> /// Sets simple property values on a DTOBaseItem /// </summary> @@ -674,8 +738,8 @@ namespace Emby.Server.Implementations.Dto dto.LockData = item.IsLocked; dto.ForcedSortName = item.ForcedSortName; } - dto.Container = item.Container; + dto.Container = item.Container; dto.EndDate = item.EndDate; if (options.ContainsField(ItemFields.ExternalUrls)) @@ -694,10 +758,12 @@ namespace Emby.Server.Implementations.Dto dto.AspectRatio = hasAspectRatio.AspectRatio; } + dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>(); + var backdropLimit = options.GetImageLimit(ImageType.Backdrop); if (backdropLimit > 0) { - dto.BackdropImageTags = GetImageTags(item, item.GetImages(ImageType.Backdrop).Take(backdropLimit).ToList()); + dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit); } if (options.ContainsField(ItemFields.ScreenshotImageTags)) @@ -705,7 +771,7 @@ namespace Emby.Server.Implementations.Dto var screenshotLimit = options.GetImageLimit(ImageType.Screenshot); if (screenshotLimit > 0) { - dto.ScreenshotImageTags = GetImageTags(item, item.GetImages(ImageType.Screenshot).Take(screenshotLimit).ToList()); + dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit); } } @@ -721,12 +787,11 @@ namespace Emby.Server.Implementations.Dto // Prevent implicitly captured closure var currentItem = item; - foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type)) - .ToList()) + foreach (var image in currentItem.ImageInfos.Where(i => !currentItem.AllowsMultipleImages(i.Type))) { if (options.GetImageLimit(image.Type) > 0) { - var tag = GetImageCacheTag(item, image); + var tag = GetTagAndFillBlurhash(dto, item, image); if (tag != null) { @@ -871,8 +936,7 @@ namespace Emby.Server.Implementations.Dto if (albumParent != null) { dto.AlbumId = albumParent.Id; - - dto.AlbumPrimaryImageTag = GetImageCacheTag(albumParent, ImageType.Primary); + dto.AlbumPrimaryImageTag = GetTagAndFillBlurhash(dto, albumParent, ImageType.Primary); } //if (options.ContainsField(ItemFields.MediaSourceCount)) @@ -1099,7 +1163,7 @@ namespace Emby.Server.Implementations.Dto episodeSeries = episodeSeries ?? episode.Series; if (episodeSeries != null) { - dto.SeriesPrimaryImageTag = GetImageCacheTag(episodeSeries, ImageType.Primary); + dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary); } } @@ -1145,7 +1209,7 @@ namespace Emby.Server.Implementations.Dto series = series ?? season.Series; if (series != null) { - dto.SeriesPrimaryImageTag = GetImageCacheTag(series, ImageType.Primary); + dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary); } } } @@ -1275,7 +1339,7 @@ namespace Emby.Server.Implementations.Dto if (image != null) { dto.ParentLogoItemId = GetDtoId(parent); - dto.ParentLogoImageTag = GetImageCacheTag(parent, image); + dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null) @@ -1285,7 +1349,7 @@ namespace Emby.Server.Implementations.Dto if (image != null) { dto.ParentArtItemId = GetDtoId(parent); - dto.ParentArtImageTag = GetImageCacheTag(parent, image); + dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && !(parent is ICollectionFolder) && !(parent is UserView)) @@ -1295,7 +1359,7 @@ namespace Emby.Server.Implementations.Dto if (image != null) { dto.ParentThumbItemId = GetDtoId(parent); - dto.ParentThumbImageTag = GetImageCacheTag(parent, image); + dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image); } } if (backdropLimit > 0 && !((dto.BackdropImageTags != null && dto.BackdropImageTags.Length > 0) || (dto.ParentBackdropImageTags != null && dto.ParentBackdropImageTags.Length > 0))) @@ -1305,7 +1369,7 @@ namespace Emby.Server.Implementations.Dto if (images.Count > 0) { dto.ParentBackdropItemId = GetDtoId(parent); - dto.ParentBackdropImageTags = GetImageTags(parent, images); + dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b69a126b3..279ec3098 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -54,6 +54,7 @@ <TargetFramework>netstandard2.1</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors> </PropertyGroup> <!-- Code Analyzers--> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index 8e3236407..9bc2b62ec 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -302,7 +302,7 @@ namespace Emby.Server.Implementations.EntryPoints .Select(x => x.First()) .ToList(); - SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None); + SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult(); if (LibraryUpdateTimer != null) { @@ -327,7 +327,7 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="foldersAddedTo">The folders added to.</param> /// <param name="foldersRemovedFrom">The folders removed from.</param> /// <param name="cancellationToken">The cancellation token.</param> - private async void SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken) + private async Task SendChangeNotifications(List<BaseItem> itemsAdded, List<BaseItem> itemsUpdated, List<BaseItem> itemsRemoved, List<Folder> foldersAddedTo, List<Folder> foldersRemovedFrom, CancellationToken cancellationToken) { var userIds = _sessionManager.Sessions .Select(i => i.UserId) diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 41c0c5115..997571a91 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -42,27 +42,27 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - private void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { - SendMessage("SeriesTimerCreated", e.Argument); + await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false); } - private void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { - SendMessage("TimerCreated", e.Argument); + await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false); } - private void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { - SendMessage("SeriesTimerCancelled", e.Argument); + await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false); } - private void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) + private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e) { - SendMessage("TimerCancelled", e.Argument); + await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false); } - private async void SendMessage(string name, TimerEventInfo info) + private async Task SendMessage(string name, TimerEventInfo info) { var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList(); diff --git a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs index b323a0a95..eb43a90f2 100644 --- a/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -86,29 +86,29 @@ namespace Emby.Server.Implementations.EntryPoints return Task.CompletedTask; } - private void OnPackageInstalling(object sender, InstallationInfo e) + private async void OnPackageInstalling(object sender, InstallationInfo e) { SendMessageToAdminSessions("PackageInstalling", e); } - private void OnPackageInstallationCancelled(object sender, InstallationInfo e) + private async void OnPackageInstallationCancelled(object sender, InstallationInfo e) { SendMessageToAdminSessions("PackageInstallationCancelled", e); } - private void OnPackageInstallationCompleted(object sender, InstallationInfo e) + private async void OnPackageInstallationCompleted(object sender, InstallationInfo e) { SendMessageToAdminSessions("PackageInstallationCompleted", e); } - private void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) + private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e) { - SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo); + await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false); } - private void OnTaskCompleted(object sender, TaskCompletionEventArgs e) + private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e) { - SendMessageToAdminSessions("ScheduledTaskEnded", e.Result); + await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false); } /// <summary> @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The e.</param> - private void OnPluginUninstalled(object sender, IPlugin e) + private async void OnPluginUninstalled(object sender, IPlugin e) { SendMessageToAdminSessions("PluginUninstalled", e); } @@ -126,9 +126,9 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> - private void OnHasPendingRestartChanged(object sender, EventArgs e) + private async void OnHasPendingRestartChanged(object sender, EventArgs e) { - _sessionManager.SendRestartRequiredNotification(CancellationToken.None); + await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false); } /// <summary> @@ -136,11 +136,11 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The e.</param> - private void OnUserUpdated(object sender, GenericEventArgs<User> e) + private async void OnUserUpdated(object sender, GenericEventArgs<User> e) { var dto = _userManager.GetUserDto(e.Argument); - SendMessageToUserSession(e.Argument, "UserUpdated", dto); + await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false); } /// <summary> @@ -148,26 +148,26 @@ namespace Emby.Server.Implementations.EntryPoints /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The e.</param> - private void OnUserDeleted(object sender, GenericEventArgs<User> e) + private async void OnUserDeleted(object sender, GenericEventArgs<User> e) { - SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)); + await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false); } - private void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) + private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e) { var dto = _userManager.GetUserDto(e.Argument); - SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto); + await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false); } - private void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e) + private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e) { var dto = _userManager.GetUserDto(e.Argument); - SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto); + await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false); } - private async void SendMessageToAdminSessions<T>(string name, T data) + private async Task SendMessageToAdminSessions<T>(string name, T data) { try { @@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.EntryPoints } } - private async void SendMessageToUserSession<T>(User user, string name, T data) + private async Task SendMessageToUserSession<T>(User user, string name, T data) { try { diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 50ba0f8fa..5bc1a81aa 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,19 +37,21 @@ 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() + public Task RunAsync() { - _udpServer = new UdpServer(_logger, _appHost); + _udpServer = new UdpServer(_logger, _appHost, _config); _udpServer.Start(PortNumber, _cancellationTokenSource.Token); + return Task.CompletedTask; } /// <inheritdoc /> diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 2e9ecc4ae..9d5969583 100644 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -56,6 +56,7 @@ namespace Emby.Server.Implementations.HttpServer /// <summary> /// Gets the result. /// </summary> + /// <param name="requestContext">The request context.</param> /// <param name="content">The content.</param> /// <param name="contentType">Type of the content.</param> /// <param name="responseHeaders">The response headers.</param> @@ -255,16 +256,20 @@ namespace Emby.Server.Implementations.HttpServer { var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - if (string.IsNullOrEmpty(acceptEncoding)) + if (!string.IsNullOrEmpty(acceptEncoding)) { - //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) + // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) // return "br"; - if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1) + if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) + { return "deflate"; + } - if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1) + if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) + { return "gzip"; + } } return null; diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 095725c50..0680c5ffe 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -78,6 +78,9 @@ namespace Emby.Server.Implementations.HttpServer /// <value>The last activity date.</value> public DateTime LastActivityDate { get; private set; } + /// <inheritdoc /> + public DateTime LastKeepAliveDate { get; set; } + /// <summary> /// Gets or sets the query string. /// </summary> @@ -218,7 +221,42 @@ namespace Emby.Server.Implementations.HttpServer Connection = this }; - await OnReceive(info).ConfigureAwait(false); + if (info.MessageType.Equals("KeepAlive", StringComparison.Ordinal)) + { + await SendKeepAliveResponse(); + } + else + { + await OnReceive(info).ConfigureAwait(false); + } + } + + private Task SendKeepAliveResponse() + { + LastKeepAliveDate = DateTime.UtcNow; + return SendAsync(new WebSocketMessage<string> + { + MessageType = "KeepAlive" + }, CancellationToken.None); + } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + _socket.Dispose(); + } } } } diff --git a/Emby.Server.Implementations/IStartupOptions.cs b/Emby.Server.Implementations/IStartupOptions.cs index 16b68170b..0b9f80538 100644 --- a/Emby.Server.Implementations/IStartupOptions.cs +++ b/Emby.Server.Implementations/IStartupOptions.cs @@ -1,3 +1,7 @@ +#pragma warning disable CS1591 + +using System; + namespace Emby.Server.Implementations { public interface IStartupOptions @@ -36,5 +40,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/DefaultAuthenticationProvider.cs b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs index 52c8facc3..02f150607 100644 --- a/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs +++ b/Emby.Server.Implementations/Library/DefaultAuthenticationProvider.cs @@ -135,43 +135,5 @@ namespace Emby.Server.Implementations.Library ? null : Hex.Encode(PasswordHash.Parse(user.EasyPassword).Hash); } - - /// <summary> - /// Gets the hashed string. - /// </summary> - public string GetHashedString(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).ToString(); - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - var salt = passwordHash.Salt.ToArray(); - return new PasswordHash( - passwordHash.Id, - _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - salt), - salt, - passwordHash.Parameters.ToDictionary(x => x.Key, y => y.Value)).ToString(); - } - - public ReadOnlySpan<byte> GetHashed(User user, string str) - { - if (string.IsNullOrEmpty(user.Password)) - { - return _cryptographyProvider.CreatePasswordHash(str).Hash; - } - - // TODO: make use of iterations parameter? - PasswordHash passwordHash = PasswordHash.Parse(user.Password); - return _cryptographyProvider.ComputeHash( - passwordHash.Id, - Encoding.UTF8.GetBytes(str), - passwordHash.Salt.ToArray()); - } } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 0b86b2db7..677030b82 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -21,6 +21,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -35,6 +36,7 @@ using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -50,7 +52,7 @@ using VideoResolver = Emby.Naming.Video.VideoResolver; namespace Emby.Server.Implementations.Library { /// <summary> - /// Class LibraryManager + /// Class LibraryManager. /// </summary> public class LibraryManager : ILibraryManager { @@ -67,6 +69,7 @@ namespace Emby.Server.Implementations.Library private readonly IFileSystem _fileSystem; private readonly IItemRepository _itemRepository; private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; + private readonly IImageProcessor _imageProcessor; private NamingOptions _namingOptions; private string[] _videoFileExtensions; @@ -135,6 +138,13 @@ namespace Emby.Server.Implementations.Library /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> /// <param name="userDataRepository">The user data repository.</param> + /// <param name="libraryMonitorFactory">The library monitor.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="providerManagerFactory">The provider manager.</param> + /// <param name="userviewManagerFactory">The userview manager.</param> + /// <param name="mediaEncoder">The media encoder.</param> + /// <param name="itemRepository">The item repository.</param> + /// <param name="imageProcessor">The image processor.</param> public LibraryManager( IServerApplicationHost appHost, ILogger<LibraryManager> logger, @@ -147,7 +157,8 @@ namespace Emby.Server.Implementations.Library Lazy<IProviderManager> providerManagerFactory, Lazy<IUserViewManager> userviewManagerFactory, IMediaEncoder mediaEncoder, - IItemRepository itemRepository) + IItemRepository itemRepository, + IImageProcessor imageProcessor) { _appHost = appHost; _logger = logger; @@ -161,6 +172,7 @@ namespace Emby.Server.Implementations.Library _userviewManagerFactory = userviewManagerFactory; _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; + _imageProcessor = imageProcessor; _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); @@ -1815,10 +1827,90 @@ namespace Emby.Server.Implementations.Library } } - public void UpdateImages(BaseItem item) + private bool ImageNeedsRefresh(ItemImageInfo image) { - _itemRepository.SaveImages(item); + if (image.Path != null && image.IsLocalFile) + { + if (image.Width == 0 || image.Height == 0 || string.IsNullOrEmpty(image.BlurHash)) + { + return true; + } + try + { + return _fileSystem.GetLastWriteTimeUtc(image.Path) != image.DateModified; + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot get file info for {0}", image.Path); + return false; + } + } + + return image.Path != null && !image.IsLocalFile; + } + + public void UpdateImages(BaseItem item, bool forceUpdate = false) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + var outdated = forceUpdate ? item.ImageInfos.Where(i => i.Path != null).ToArray() : item.ImageInfos.Where(ImageNeedsRefresh).ToArray(); + if (outdated.Length == 0) + { + RegisterItem(item); + return; + } + + foreach (var img in outdated) + { + var image = img; + if (!img.IsLocalFile) + { + try + { + var index = item.GetImageIndex(img); + image = ConvertImageToLocal(item, img, index).ConfigureAwait(false).GetAwaiter().GetResult(); + } + catch (ArgumentException) + { + _logger.LogWarning("Cannot get image index for {0}", img.Path); + continue; + } + catch (InvalidOperationException) + { + _logger.LogWarning("Cannot fetch image from {0}", img.Path); + continue; + } + } + + ImageDimensions size = _imageProcessor.GetImageDimensions(item, image); + image.Width = size.Width; + image.Height = size.Height; + + try + { + image.BlurHash = _imageProcessor.GetImageBlurHash(image.Path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path); + image.BlurHash = string.Empty; + } + + try + { + image.DateModified = _fileSystem.GetLastWriteTimeUtc(image.Path); + } + catch (Exception ex) + { + _logger.LogError(ex, "Cannot update DateModified for {0}", image.Path); + } + } + + _itemRepository.SaveImages(item); RegisterItem(item); } @@ -1839,7 +1931,7 @@ namespace Emby.Server.Implementations.Library item.DateLastSaved = DateTime.UtcNow; - RegisterItem(item); + UpdateImages(item, updateReason >= ItemUpdateType.ImageUpdate); } _itemRepository.SaveItems(itemsList, cancellationToken); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 01fe98f3a..a5e5981b8 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -521,11 +521,7 @@ namespace Emby.Server.Implementations.Library SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); } - return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse - { - MediaSource = clone - - }, liveStream as IDirectStreamProvider); + return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); } private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) diff --git a/Emby.Server.Implementations/Library/UserManager.cs b/Emby.Server.Implementations/Library/UserManager.cs index b8feb5535..140155d0e 100644 --- a/Emby.Server.Implementations/Library/UserManager.cs +++ b/Emby.Server.Implementations/Library/UserManager.cs @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library /// <param name="user">The user.</param> private void OnUserUpdated(User user) { - UserUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + UserUpdated?.Invoke(this, new GenericEventArgs<User>(user)); } /// <summary> @@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Library /// <param name="user">The user.</param> private void OnUserDeleted(User user) { - UserDeleted?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + UserDeleted?.Invoke(this, new GenericEventArgs<User>(user)); } public NameIdPair[] GetAuthenticationProviders() @@ -608,31 +608,6 @@ namespace Emby.Server.Implementations.Library return dto; } - public PublicUserDto GetPublicUserDto(User user, string remoteEndPoint = null) - { - if (user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - IAuthenticationProvider authenticationProvider = GetAuthenticationProvider(user); - bool hasConfiguredPassword = authenticationProvider.HasPassword(user); - bool hasConfiguredEasyPassword = !string.IsNullOrEmpty(authenticationProvider.GetEasyPasswordHash(user)); - - bool hasPassword = user.Configuration.EnableLocalPassword && - !string.IsNullOrEmpty(remoteEndPoint) && - _networkManager.IsInLocalNetwork(remoteEndPoint) ? hasConfiguredEasyPassword : hasConfiguredPassword; - - PublicUserDto dto = new PublicUserDto - { - Name = user.Name, - HasPassword = hasPassword, - HasConfiguredPassword = hasConfiguredPassword, - }; - - return dto; - } - public UserDto GetOfflineUserDto(User user) { var dto = GetUserDto(user); @@ -776,7 +751,7 @@ namespace Emby.Server.Implementations.Library _userRepository.CreateUser(user); - EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User> { Argument = user }, _logger); + EventHelper.QueueEventIfNotNull(UserCreated, this, new GenericEventArgs<User>(user), _logger); return user; } @@ -1001,7 +976,7 @@ namespace Emby.Server.Implementations.Library if (fireEvent) { - UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + UserPolicyUpdated?.Invoke(this, new GenericEventArgs<User>(user)); } } @@ -1071,7 +1046,7 @@ namespace Emby.Server.Implementations.Library if (fireEvent) { - UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User> { Argument = user }); + UserConfigurationUpdated?.Invoke(this, new GenericEventArgs<User>(user)); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 3efe1ee25..5a5dc3329 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -140,11 +140,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) + private async void OnNamedConfigurationUpdated(object sender, ConfigurationUpdateEventArgs e) { if (string.Equals(e.Key, "livetv", StringComparison.OrdinalIgnoreCase)) { - OnRecordingFoldersChanged(); + await CreateRecordingFolders().ConfigureAwait(false); } } @@ -155,11 +155,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return CreateRecordingFolders(); } - private async void OnRecordingFoldersChanged() - { - await CreateRecordingFolders().ConfigureAwait(false); - } - internal async Task CreateRecordingFolders() { try @@ -1334,7 +1329,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await CreateRecordingFolders().ConfigureAwait(false); TriggerRefresh(recordPath); - EnforceKeepUpTo(timer, seriesPath); + await EnforceKeepUpTo(timer, seriesPath).ConfigureAwait(false); }; await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); @@ -1494,7 +1489,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return item; } - private async void EnforceKeepUpTo(TimerInfo timer, string seriesPath) + private async Task EnforceKeepUpTo(TimerInfo timer, string seriesPath) { if (string.IsNullOrWhiteSpace(timer.SeriesTimerId)) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index bc86cc59a..70dd8f321 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV onStarted(); // Important - don't await the log task or we won't be able to kill ffmpeg when the user stops playback - StartStreamingLog(_process.StandardError.BaseStream, _logFileStream); + _ = StartStreamingLog(_process.StandardError.BaseStream, _logFileStream); _logger.LogInformation("ffmpeg recording process started for {0}", _targetPath); @@ -321,7 +321,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private async void StartStreamingLog(Stream source, Stream target) + private async Task StartStreamingLog(Stream source, Stream target) { try { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs index 7ebb043d8..285a59a24 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs @@ -109,7 +109,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (startDate < now) { - TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = item }); + TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(item)); return; } @@ -151,7 +151,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var timer = GetAll().FirstOrDefault(i => string.Equals(i.Id, timerId, StringComparison.OrdinalIgnoreCase)); if (timer != null) { - TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo> { Argument = timer }); + TimerFired?.Invoke(this, new GenericEventArgs<TimerInfo>(timer)); } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index 1b10f2d27..dbd0e6f2e 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -10,10 +10,8 @@ using Emby.Server.Implementations.Library; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; @@ -148,27 +146,18 @@ namespace Emby.Server.Implementations.LiveTv { var timerId = e.Argument; - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo - { - Id = timerId - } - }); + TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(timerId))); } private void OnEmbyTvTimerCreated(object sender, GenericEventArgs<TimerInfo> e) { var timer = e.Argument; - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo + TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( + new TimerEventInfo(timer.Id) { - ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId), - Id = timer.Id - } - }); + ProgramId = _tvDtoService.GetInternalProgramId(timer.ProgramId) + })); } public List<NameIdPair> GetTunerHostTypes() @@ -788,22 +777,12 @@ namespace Emby.Server.Implementations.LiveTv if (query.OrderBy.Count == 0) { - if (query.IsAiring ?? false) - { - // Unless something else was specified, order by start date to take advantage of a specialized index - query.OrderBy = new[] - { - (ItemSortBy.StartDate, SortOrder.Ascending) - }; - } - else + + // Unless something else was specified, order by start date to take advantage of a specialized index + query.OrderBy = new[] { - // Unless something else was specified, order by start date to take advantage of a specialized index - query.OrderBy = new[] - { - (ItemSortBy.StartDate, SortOrder.Ascending) - }; - } + (ItemSortBy.StartDate, SortOrder.Ascending) + }; } RemoveFields(options); @@ -1734,13 +1713,7 @@ namespace Emby.Server.Implementations.LiveTv if (!(service is EmbyTV.EmbyTV)) { - TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo - { - Id = id - } - }); + TimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); } } @@ -1757,13 +1730,7 @@ namespace Emby.Server.Implementations.LiveTv await service.CancelSeriesTimerAsync(timer.ExternalId, CancellationToken.None).ConfigureAwait(false); - SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo - { - Id = id - } - }); + SeriesTimerCancelled?.Invoke(this, new GenericEventArgs<TimerEventInfo>(new TimerEventInfo(id))); } public async Task<TimerInfoDto> GetTimer(string id, CancellationToken cancellationToken) @@ -2082,14 +2049,11 @@ namespace Emby.Server.Implementations.LiveTv if (!(service is EmbyTV.EmbyTV)) { - TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo + TimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( + new TimerEventInfo(newTimerId) { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId), - Id = newTimerId - } - }); + ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) + })); } } @@ -2114,14 +2078,11 @@ namespace Emby.Server.Implementations.LiveTv await service.CreateSeriesTimerAsync(info, cancellationToken).ConfigureAwait(false); } - SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo> - { - Argument = new TimerEventInfo + SeriesTimerCreated?.Invoke(this, new GenericEventArgs<TimerEventInfo>( + new TimerEventInfo(newTimerId) { - ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId), - Id = newTimerId - } - }); + ProgramId = _tvDtoService.GetInternalProgramId(info.ProgramId) + })); } public async Task UpdateTimer(TimerInfoDto timer, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs index 1056a33b9..8e7d60a15 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshChannelsScheduledTask.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json new file mode 100644 index 000000000..b0fdc8386 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -0,0 +1,117 @@ +{ + "LabelRunningTimeValue": "Duración: {0}", + "ValueSpecialEpisodeName": "Especial - {0}", + "Sync": "Sincronizar", + "Songs": "Canciones", + "Shows": "Programas", + "Playlists": "Listas de reproducción", + "Photos": "Fotos", + "Movies": "Películas", + "HeaderNextUp": "A continuación", + "HeaderLiveTV": "TV en vivo", + "HeaderFavoriteSongs": "Canciones favoritas", + "HeaderFavoriteArtists": "Artistas favoritos", + "HeaderFavoriteAlbums": "Álbumes favoritos", + "HeaderFavoriteEpisodes": "Episodios favoritos", + "HeaderFavoriteShows": "Programas favoritos", + "HeaderContinueWatching": "Continuar viendo", + "HeaderAlbumArtists": "Artistas del álbum", + "Genres": "Géneros", + "Folders": "Carpetas", + "Favorites": "Favoritos", + "Collections": "Colecciones", + "Channels": "Canales", + "Books": "Libros", + "Artists": "Artistas", + "Albums": "Álbumes", + "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": "Elimina archivos transcodificados que tengan más de un día.", + "TaskCleanTranscode": "Limpiar directorio de transcodificado", + "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.", + "TaskUpdatePlugins": "Actualizar complementos", + "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", + "TasksMaintenanceCategory": "Mantenimiento", + "VersionNumber": "Versión {0}", + "ValueHasBeenAddedToLibrary": "{0} se ha añadido a tu biblioteca de medios", + "UserStoppedPlayingItemWithValues": "{0} ha terminado de reproducir {1} en {2}", + "UserStartedPlayingItemWithValues": "{0} está reproduciendo {1} en {2}", + "UserPolicyUpdatedWithName": "La política de usuario ha sido actualizada para {0}", + "UserPasswordChangedWithName": "Se ha cambiado la contraseña para el usuario {0}", + "UserOnlineFromDevice": "{0} está en línea desde {1}", + "UserOfflineFromDevice": "{0} se ha desconectado desde {1}", + "UserLockedOutWithName": "El usuario {0} ha sido bloqueado", + "UserDownloadingItemWithValues": "{0} está descargando {1}", + "UserDeletedWithName": "El usuario {0} ha sido eliminado", + "UserCreatedWithName": "El usuario {0} ha sido creado", + "User": "Usuario", + "TvShows": "Programas de TV", + "System": "Sistema", + "SubtitleDownloadFailureFromForItem": "Falló la descarga de subtítulos desde {0} para {1}", + "StartupEmbyServerIsLoading": "El servidor Jellyfin está cargando. Por favor, intente de nuevo pronto.", + "ServerNameNeedsToBeRestarted": "{0} debe ser reiniciado", + "ScheduledTaskStartedWithName": "{0} iniciado", + "ScheduledTaskFailedWithName": "{0} falló", + "ProviderValue": "Proveedor: {0}", + "PluginUpdatedWithName": "{0} fue actualizado", + "PluginUninstalledWithName": "{0} fue desinstalado", + "PluginInstalledWithName": "{0} fue instalado", + "Plugin": "Complemento", + "NotificationOptionVideoPlaybackStopped": "Reproducción de video detenida", + "NotificationOptionVideoPlayback": "Reproducción de video iniciada", + "NotificationOptionUserLockedOut": "Usuario bloqueado", + "NotificationOptionTaskFailed": "Falla de tarea programada", + "NotificationOptionServerRestartRequired": "Se necesita reiniciar el servidor", + "NotificationOptionPluginUpdateInstalled": "Actualización de complemento instalada", + "NotificationOptionPluginUninstalled": "Complemento desinstalado", + "NotificationOptionPluginInstalled": "Complemento instalado", + "NotificationOptionPluginError": "Falla de complemento", + "NotificationOptionNewLibraryContent": "Nuevo contenido agregado", + "NotificationOptionInstallationFailed": "Falla de instalación", + "NotificationOptionCameraImageUploaded": "Imagen de la cámara subida", + "NotificationOptionAudioPlaybackStopped": "Reproducción de audio detenida", + "NotificationOptionAudioPlayback": "Reproducción de audio iniciada", + "NotificationOptionApplicationUpdateInstalled": "Actualización de la aplicación instalada", + "NotificationOptionApplicationUpdateAvailable": "Actualización de la aplicación disponible", + "NewVersionIsAvailable": "Una nueva versión del Servidor Jellyfin está disponible para descargar.", + "NameSeasonUnknown": "Temporada desconocida", + "NameSeasonNumber": "Temporada {0}", + "NameInstallFailed": "Falló la instalación de {0}", + "MusicVideos": "Videos musicales", + "Music": "Música", + "MixedContent": "Contenido mezclado", + "MessageServerConfigurationUpdated": "Se ha actualizado la configuración del servidor", + "MessageNamedServerConfigurationUpdatedWithValue": "Se ha actualizado la sección {0} de la configuración del servidor", + "MessageApplicationUpdatedTo": "El servidor Jellyfin ha sido actualizado a {0}", + "MessageApplicationUpdated": "El servidor Jellyfin ha sido actualizado", + "Latest": "Recientes", + "LabelIpAddressValue": "Dirección IP: {0}", + "ItemRemovedWithName": "{0} fue removido de la biblioteca", + "ItemAddedWithName": "{0} fue agregado a la biblioteca", + "Inherit": "Heredar", + "HomeVideos": "Videos caseros", + "HeaderRecordingGroups": "Grupos de grabación", + "HeaderCameraUploads": "Subidas desde la cámara", + "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}", + "DeviceOnlineWithName": "{0} está conectado", + "DeviceOfflineWithName": "{0} se ha desconectado", + "ChapterNameValue": "Capítulo {0}", + "CameraImageUploadedFrom": "Una nueva imagen de cámara ha sido subida desde {0}", + "AuthenticationSucceededWithUserName": "{0} autenticado con éxito", + "Application": "Aplicación", + "AppDeviceValues": "App: {0}, Dispositivo: {1}" +} diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index 150952d8b..47ebe1254 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -5,7 +5,7 @@ "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", @@ -15,7 +15,7 @@ "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", - "HeaderAlbumArtists": "Artistes de l'album", + "HeaderAlbumArtists": "Artistes", "HeaderCameraUploads": "Photos transférées", "HeaderContinueWatching": "Continuer à regarder", "HeaderFavoriteAlbums": "Albums favoris", diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json new file mode 100644 index 000000000..f722dd8c0 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -0,0 +1,99 @@ +{ + "VersionNumber": "பதிப்பு {0}", + "ValueSpecialEpisodeName": "சிறப்பு - {0}", + "TasksMaintenanceCategory": "பராமரிப்பு", + "TaskCleanCache": "தற்காலிக சேமிப்பு கோப்பகத்தை சுத்தம் செய்யவும்", + "TaskRefreshChapterImages": "அத்தியாயப் படங்களை பிரித்தெடுக்கவும்", + "TaskRefreshPeople": "மக்களைப் புதுப்பிக்கவும்", + "TaskCleanTranscode": "டிரான்ஸ்கோட் கோப்பகத்தை சுத்தம் செய்யவும்", + "TaskRefreshChannelsDescription": "இணையச் சேனல் தகவல்களைப் புதுப்பிக்கிறது.", + "System": "ஒருங்கியம்", + "NotificationOptionTaskFailed": "திட்டமிடப்பட்ட பணி தோல்வியடைந்தது", + "NotificationOptionPluginUpdateInstalled": "உட்செருகி புதுப்பிக்கப்பட்டது", + "NotificationOptionPluginUninstalled": "உட்செருகி நீக்கப்பட்டது", + "NotificationOptionPluginInstalled": "உட்செருகி நிறுவப்பட்டது", + "NotificationOptionPluginError": "உட்செருகி செயலிழந்தது", + "NotificationOptionCameraImageUploaded": "புகைப்படம் பதிவேற்றப்பட்டது", + "MixedContent": "கலப்பு உள்ளடக்கங்கள்", + "MessageServerConfigurationUpdated": "சேவையக அமைப்புகள் புதுப்பிக்கப்பட்டன", + "MessageApplicationUpdatedTo": "ஜெல்லிஃபின் சேவையகம் {0} இற்கு புதுப்பிக்கப்பட்டது", + "MessageApplicationUpdated": "ஜெல்லிஃபின் சேவையகம் புதுப்பிக்கப்பட்டது", + "Inherit": "மரபரிமையாகப் பெறு", + "HeaderRecordingGroups": "பதிவு குழுக்கள்", + "HeaderCameraUploads": "புகைப்பட பதிவேற்றங்கள்", + "Folders": "கோப்புறைகள்", + "FailedLoginAttemptWithUserName": "{0} இலிருந்து உள்நுழைவு முயற்சி தோல்வியடைந்தது", + "DeviceOnlineWithName": "{0} இணைக்கப்பட்டது", + "DeviceOfflineWithName": "{0} துண்டிக்கப்பட்டது", + "Collections": "தொகுப்புகள்", + "CameraImageUploadedFrom": "{0} இலிருந்து புதிய புகைப்படம் பதிவேற்றப்பட்டது", + "AppDeviceValues": "செயலி: {0}, சாதனம்: {1}", + "TaskDownloadMissingSubtitles": "விடுபட்டுபோன வசன வரிகளைப் பதிவிறக்கு", + "TaskRefreshChannels": "சேனல்களை புதுப்பி", + "TaskUpdatePlugins": "உட்செருகிகளை புதுப்பி", + "TaskRefreshLibrary": "மீடியா நூலகத்தை ஆராய்", + "TasksChannelsCategory": "இணைய சேனல்கள்", + "TasksApplicationCategory": "செயலி", + "TasksLibraryCategory": "நூலகம்", + "UserPolicyUpdatedWithName": "பயனர் கொள்கை {0} இற்கு புதுப்பிக்கப்பட்டுள்ளது", + "UserPasswordChangedWithName": "{0} பயனருக்கு கடவுச்சொல் மாற்றப்பட்டுள்ளது", + "UserLockedOutWithName": "பயனர் {0} முடக்கப்பட்டார்", + "UserDownloadingItemWithValues": "{0} ஆல் {1} பதிவிறக்கப்படுகிறது", + "UserDeletedWithName": "பயனர் {0} நீக்கப்பட்டார்", + "UserCreatedWithName": "பயனர் {0} உருவாக்கப்பட்டார்", + "User": "பயனர்", + "TvShows": "தொலைக்காட்சித் தொடர்கள்", + "Sync": "ஒத்திசைவு", + "StartupEmbyServerIsLoading": "ஜெல்லிஃபின் சேவையகம் துவங்குகிறது. சிறிது நேரம் கழித்து முயற்சிக்கவும்.", + "Songs": "பாட்டுகள்", + "Shows": "தொடர்கள்", + "ServerNameNeedsToBeRestarted": "{0} மறுதொடக்கம் செய்யப்பட வேண்டும்", + "ScheduledTaskStartedWithName": "{0} துவங்கியது", + "ScheduledTaskFailedWithName": "{0} தோல்வியடைந்தது", + "ProviderValue": "வழங்குநர்: {0}", + "PluginUpdatedWithName": "{0} புதுப்பிக்கப்பட்டது", + "PluginUninstalledWithName": "{0} நீக்கப்பட்டது", + "PluginInstalledWithName": "{0} நிறுவப்பட்டது", + "Plugin": "உட்செருகி", + "Playlists": "தொடர் பட்டியல்கள்", + "Photos": "புகைப்படங்கள்", + "NotificationOptionVideoPlaybackStopped": "நிகழ்பட ஒளிபரப்பு நிறுத்தப்பட்டது", + "NotificationOptionVideoPlayback": "நிகழ்பட ஒளிபரப்பு துவங்கியது", + "NotificationOptionUserLockedOut": "பயனர் கணக்கு முடக்கப்பட்டது", + "NotificationOptionServerRestartRequired": "சேவையக மறுதொடக்கம் தேவை", + "NotificationOptionNewLibraryContent": "புதிய உள்ளடக்கங்கள் சேர்க்கப்பட்டன", + "NotificationOptionInstallationFailed": "நிறுவல் தோல்வியடைந்தது", + "NotificationOptionAudioPlaybackStopped": "ஒலி இசைத்தல் நிறுத்தப்பட்டது", + "NotificationOptionAudioPlayback": "ஒலி இசைக்கத் துவங்கியுள்ளது", + "NotificationOptionApplicationUpdateInstalled": "செயலி புதுப்பிக்கப்பட்டது", + "NotificationOptionApplicationUpdateAvailable": "செயலியினை புதுப்பிக்கலாம்", + "NameSeasonUnknown": "பருவம் அறியப்படாதவை", + "NameSeasonNumber": "பருவம் {0}", + "NameInstallFailed": "{0} நிறுவல் தோல்வியடைந்தது", + "MusicVideos": "இசைப்படங்கள்", + "Music": "இசை", + "Movies": "திரைப்படங்கள்", + "Latest": "புதியன", + "LabelRunningTimeValue": "ஓடும் நேரம்: {0}", + "LabelIpAddressValue": "ஐபி முகவரி: {0}", + "ItemRemovedWithName": "{0} நூலகத்திலிருந்து அகற்றப்பட்டது", + "ItemAddedWithName": "{0} நூலகத்தில் சேர்க்கப்பட்டது", + "HeaderNextUp": "அடுத்ததாக", + "HeaderLiveTV": "நேரடித் தொலைக்காட்சி", + "HeaderFavoriteSongs": "பிடித்த பாட்டுகள்", + "HeaderFavoriteShows": "பிடித்த தொடர்கள்", + "HeaderFavoriteEpisodes": "பிடித்த அத்தியாயங்கள்", + "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்", + "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்", + "HeaderContinueWatching": "தொடர்ந்து பார்", + "HeaderAlbumArtists": "இசைக் கலைஞர்கள்", + "Genres": "வகைகள்", + "Favorites": "பிடித்தவை", + "ChapterNameValue": "அத்தியாயம் {0}", + "Channels": "சேனல்கள்", + "Books": "புத்தகங்கள்", + "AuthenticationSucceededWithUserName": "{0} வெற்றிகரமாக அங்கீகரிக்கப்பட்டது", + "Artists": "கலைஞர்கள்", + "Application": "செயலி", + "Albums": "ஆல்பங்கள்" +} diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 677d68b4c..7b7575707 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index e42ff8496..f347540c7 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Net; using System.Net.Sockets; diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index 211ca6784..848f82d85 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Net; using System.Net.Sockets; diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index b3e88b667..d1a28e7a1 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs index cd9f7946e..889760586 100644 --- a/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/ManualPlaylistsFolder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; diff --git a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs index bb56d9771..f8a2d9741 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.Images; @@ -32,9 +34,7 @@ namespace Emby.Server.Implementations.Playlists { var subItem = i.Item2; - var episode = subItem as Episode; - - if (episode != null) + if (subItem is Episode episode) { var series = episode.Series; if (series != null && series.HasImage(ImageType.Primary)) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 9b1510ac9..c51eb0586 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -153,10 +155,7 @@ namespace Emby.Server.Implementations.Playlists }); } - return new PlaylistCreationResult - { - Id = playlist.Id.ToString("N", CultureInfo.InvariantCulture) - }; + return new PlaylistCreationResult(playlist.Id.ToString("N", CultureInfo.InvariantCulture)); } finally { diff --git a/Emby.Server.Implementations/ResourceFileManager.cs b/Emby.Server.Implementations/ResourceFileManager.cs index 6eda2b503..d192be921 100644 --- a/Emby.Server.Implementations/ResourceFileManager.cs +++ b/Emby.Server.Implementations/ResourceFileManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using MediaBrowser.Controller; diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 5b188d962..e58c335a8 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; @@ -51,7 +53,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <value>The task manager.</value> private ITaskManager TaskManager { get; set; } - private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. @@ -72,24 +73,28 @@ namespace Emby.Server.Implementations.ScheduledTasks /// or /// logger /// </exception> - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem) + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger) { if (scheduledTask == null) { throw new ArgumentNullException(nameof(scheduledTask)); } + if (applicationPaths == null) { throw new ArgumentNullException(nameof(applicationPaths)); } + if (taskManager == null) { throw new ArgumentNullException(nameof(taskManager)); } + if (jsonSerializer == null) { throw new ArgumentNullException(nameof(jsonSerializer)); } + if (logger == null) { throw new ArgumentNullException(nameof(logger)); @@ -100,7 +105,6 @@ namespace Emby.Server.Implementations.ScheduledTasks TaskManager = taskManager; JsonSerializer = jsonSerializer; Logger = logger; - _fileSystem = fileSystem; InitTriggerEvents(); } @@ -392,7 +396,7 @@ namespace Emby.Server.Implementations.ScheduledTasks ((TaskManager)TaskManager).OnTaskExecuting(this); - progress.ProgressChanged += progress_ProgressChanged; + progress.ProgressChanged += OnProgressChanged; TaskCompletionStatus status; CurrentExecutionStartTime = DateTime.UtcNow; @@ -426,7 +430,7 @@ namespace Emby.Server.Implementations.ScheduledTasks var startTime = CurrentExecutionStartTime; var endTime = DateTime.UtcNow; - progress.ProgressChanged -= progress_ProgressChanged; + progress.ProgressChanged -= OnProgressChanged; CurrentCancellationTokenSource.Dispose(); CurrentCancellationTokenSource = null; CurrentProgress = null; @@ -439,16 +443,13 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> /// <param name="sender">The sender.</param> /// <param name="e">The e.</param> - void progress_ProgressChanged(object sender, double e) + private void OnProgressChanged(object sender, double e) { e = Math.Min(e, 100); CurrentProgress = e; - TaskProgress?.Invoke(this, new GenericEventArgs<double> - { - Argument = e - }); + TaskProgress?.Invoke(this, new GenericEventArgs<double>(e)); } /// <summary> @@ -576,6 +577,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="startTime">The start time.</param> /// <param name="endTime">The end time.</param> /// <param name="status">The status.</param> + /// <param name="ex">The exception.</param> private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex) { var elapsedTime = endTime - startTime; diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 6ffa581a9..efefa5506 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -199,7 +201,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="tasks">The tasks.</param> public void AddTasks(IEnumerable<IScheduledTask> tasks) { - var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger, _fileSystem)); + var list = tasks.Select(t => new ScheduledTaskWorker(t, _applicationPaths, this, _jsonSerializer, _logger)); ScheduledTasks = ScheduledTasks.Concat(list).ToArray(); } @@ -240,10 +242,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="task">The task.</param> internal void OnTaskExecuting(IScheduledTaskWorker task) { - TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker> - { - Argument = task - }); + TaskExecuting?.Invoke(this, new GenericEventArgs<IScheduledTaskWorker>(task)); } /// <summary> @@ -253,11 +252,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="result">The result.</param> internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result) { - TaskCompleted?.Invoke(task, new TaskCompletionEventArgs - { - Result = result, - Task = task - }); + TaskCompleted?.Invoke(task, new TaskCompletionEventArgs(task, result)); ExecuteQueuedTasks(); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index ea6a70615..fae049914 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -169,18 +169,25 @@ namespace Emby.Server.Implementations.ScheduledTasks } } + /// <inheritdoc /> public string Name => _localization.GetLocalizedString("TaskRefreshChapterImages"); + /// <inheritdoc /> public string Description => _localization.GetLocalizedString("TaskRefreshChapterImagesDescription"); + /// <inheritdoc /> public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> public string Key => "RefreshChapterImages"; + /// <inheritdoc /> public bool IsHidden => false; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 9df7c538b..a6c13eaef 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -165,18 +165,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } + /// <inheritdoc /> public string Name => _localization.GetLocalizedString("TaskCleanCache"); + /// <inheritdoc /> public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + /// <inheritdoc /> public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> public string Key => "DeleteCacheFiles"; + /// <inheritdoc /> public bool IsHidden => false; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index 3140aa489..402b39a26 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -28,6 +28,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> + /// <param name="fileSystem">The file system.</param> + /// <param name="localization">The localization manager.</param> public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem, ILocalizationManager localization) { ConfigurationManager = configurationManager; @@ -82,18 +84,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } + /// <inheritdoc /> public string Name => _localization.GetLocalizedString("TaskCleanLogs"); + /// <inheritdoc /> public string Description => string.Format(_localization.GetLocalizedString("TaskCleanLogsDescription"), ConfigurationManager.CommonConfiguration.LogFileRetentionDays); + /// <inheritdoc /> public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> public string Key => "CleanLogFiles"; + /// <inheritdoc /> public bool IsHidden => false; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 1d133dcda..0d36b82c0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -132,18 +132,25 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } } + /// <inheritdoc /> public string Name => _localization.GetLocalizedString("TaskCleanTranscode"); + /// <inheritdoc /> public string Description => _localization.GetLocalizedString("TaskCleanTranscodeDescription"); + /// <inheritdoc /> public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + /// <inheritdoc /> public string Key => "DeleteTranscodeFiles"; + /// <inheritdoc /> public bool IsHidden => false; + /// <inheritdoc /> public bool IsEnabled => false; + /// <inheritdoc /> public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 63f867bf6..c384cf4bb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -1,8 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Controller; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Globalization; @@ -18,19 +19,16 @@ namespace Emby.Server.Implementations.ScheduledTasks /// The library manager. /// </summary> private readonly ILibraryManager _libraryManager; - - private readonly IServerApplicationHost _appHost; private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="PeopleValidationTask" /> class. /// </summary> /// <param name="libraryManager">The library manager.</param> - /// <param name="appHost">The server application host</param> - public PeopleValidationTask(ILibraryManager libraryManager, IServerApplicationHost appHost, ILocalizationManager localization) + /// <param name="localization">The localization manager.</param> + public PeopleValidationTask(ILibraryManager libraryManager, ILocalizationManager localization) { _libraryManager = libraryManager; - _appHost = appHost; _localization = localization; } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index e7a5e2e3d..acab3aeea 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index 74cb01444..e470adcf4 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -1,9 +1,10 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Library; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Globalization; @@ -16,20 +17,19 @@ namespace Emby.Server.Implementations.ScheduledTasks public class RefreshMediaLibraryTask : IScheduledTask { /// <summary> - /// The _library manager + /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; private readonly ILocalizationManager _localization; /// <summary> /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class. /// </summary> /// <param name="libraryManager">The library manager.</param> - public RefreshMediaLibraryTask(ILibraryManager libraryManager, IServerConfigurationManager config, ILocalizationManager localization) + /// <param name="localization">The localization manager.</param> + public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) { _libraryManager = libraryManager; - _config = config; _localization = localization; } @@ -61,18 +61,25 @@ namespace Emby.Server.Implementations.ScheduledTasks return ((LibraryManager)_libraryManager).ValidateMediaLibraryInternal(progress, cancellationToken); } + /// <inheritdoc /> public string Name => _localization.GetLocalizedString("TaskRefreshLibrary"); + /// <inheritdoc /> public string Description => _localization.GetLocalizedString("TaskRefreshLibraryDescription"); + /// <inheritdoc /> public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + /// <inheritdoc /> public string Key => "RefreshLibrary"; + /// <inheritdoc /> public bool IsHidden => false; + /// <inheritdoc /> public bool IsEnabled => true; + /// <inheritdoc /> public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index ea278de0d..c7819d4c0 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -31,6 +31,8 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Stars waiting for the trigger action /// </summary> /// <param name="lastResult">The last result.</param> + /// <param name="logger">The logger.</param> + /// <param name="taskName">The name of the task.</param> /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup) { @@ -77,10 +79,7 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 3a34da3af..74cd4ef1e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -34,6 +34,8 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Stars waiting for the trigger action /// </summary> /// <param name="lastResult">The last result.</param> + /// <param name="logger">The logger.</param> + /// <param name="taskName">The name of the task.</param> /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 08ff4f55f..e171a9e9f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Threading.Tasks; using MediaBrowser.Model.Tasks; @@ -6,7 +8,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.ScheduledTasks { /// <summary> - /// Class StartupTaskTrigger + /// Class StartupTaskTrigger. /// </summary> public class StartupTrigger : ITaskTrigger { @@ -26,6 +28,8 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Stars waiting for the trigger action /// </summary> /// <param name="lastResult">The last result.</param> + /// <param name="logger">The logger.</param> + /// <param name="taskName">The name of the task.</param> /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 2a6a7b13c..ad0b57af6 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -37,6 +37,8 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Stars waiting for the trigger action /// </summary> /// <param name="lastResult">The last result.</param> + /// <param name="logger">The logger.</param> + /// <param name="taskName">The name of the task.</param> /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup) { diff --git a/Emby.Server.Implementations/Security/AuthenticationRepository.cs b/Emby.Server.Implementations/Security/AuthenticationRepository.cs index 4e4029f06..750890ec8 100644 --- a/Emby.Server.Implementations/Security/AuthenticationRepository.cs +++ b/Emby.Server.Implementations/Security/AuthenticationRepository.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/Serialization/JsonSerializer.cs b/Emby.Server.Implementations/Serialization/JsonSerializer.cs index bcc814daf..5ec3a735a 100644 --- a/Emby.Server.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Server.Implementations/Serialization/JsonSerializer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; @@ -11,6 +13,9 @@ namespace Emby.Server.Implementations.Serialization /// </summary> public class JsonSerializer : IJsonSerializer { + /// <summary> + /// Initializes a new instance of the <see cref="JsonSerializer" /> class. + /// </summary> public JsonSerializer() { ServiceStack.Text.JsConfig.DateHandler = ServiceStack.Text.DateHandler.ISO8601; diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs index 095193828..8ba86f756 100644 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ b/Emby.Server.Implementations/Services/HttpResult.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using System.IO; using System.Net; diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs index 2563cac99..1f9c7fc22 100644 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ b/Emby.Server.Implementations/Services/RequestHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.IO; using System.Threading.Tasks; @@ -43,10 +45,7 @@ namespace Emby.Server.Implementations.Services private static string GetContentTypeWithoutEncoding(string contentType) { - return contentType == null - ? null - : contentType.Split(';')[0].ToLowerInvariant().Trim(); + return contentType?.Split(';')[0].ToLowerInvariant().Trim(); } - } } diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs index a566b18dd..f2b1d06f3 100644 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ b/Emby.Server.Implementations/Services/ResponseHelper.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Globalization; using System.IO; @@ -43,8 +45,7 @@ namespace Emby.Server.Implementations.Services response.StatusCode = httpResult.Status; } - var responseOptions = result as IHasHeaders; - if (responseOptions != null) + if (result is IHasHeaders responseOptions) { foreach (var responseHeaders in responseOptions.Headers) { diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index e24a95dbb..ad6015c1c 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index 9f5f97028..606f2a240 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 934560de3..7f44357e1 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Reflection; diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs index 5018bf4a2..59ee5908f 100644 --- a/Emby.Server.Implementations/Services/ServiceMethod.cs +++ b/Emby.Server.Implementations/Services/ServiceMethod.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; namespace Emby.Server.Implementations.Services diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index 27c4dcba0..278379a92 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs index 56e23d549..ab22fe019 100644 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Reflection; diff --git a/Emby.Server.Implementations/Services/SwaggerService.cs b/Emby.Server.Implementations/Services/SwaggerService.cs index 5177251c3..16142a70d 100644 --- a/Emby.Server.Implementations/Services/SwaggerService.cs +++ b/Emby.Server.Implementations/Services/SwaggerService.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Linq; diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs index 483c63ade..e3b6aa197 100644 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ b/Emby.Server.Implementations/Services/UrlExtensions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Common.Extensions; diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index df98a35bc..5c480e842 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -25,6 +27,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 @@ -1153,6 +1156,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); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index d4e4ba1f2..e7b4b0ec3 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -1,8 +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.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -14,6 +19,21 @@ namespace Emby.Server.Implementations.Session 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; @@ -27,6 +47,26 @@ namespace Emby.Server.Implementations.Session private readonly IHttpServer _httpServer; /// <summary> + /// The KeepAlive cancellation token. + /// </summary> + private CancellationTokenSource _keepAliveCancellationToken; + + /// <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> @@ -47,12 +87,13 @@ namespace Emby.Server.Implementations.Session httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; } - private void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) + private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) { var session = GetSession(e.Argument.QueryString, e.Argument.RemoteEndPoint.ToString()); if (session != null) { EnsureController(session, e.Argument); + await KeepAliveWebSocket(e.Argument); } else { @@ -81,6 +122,7 @@ namespace Emby.Server.Implementations.Session public void Dispose() { _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + StopKeepAlive(); } /// <summary> @@ -99,5 +141,206 @@ namespace Emby.Server.Implementations.Session 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/SocketSharp/HttpFile.cs b/Emby.Server.Implementations/SocketSharp/HttpFile.cs deleted file mode 100644 index 120ac50d9..000000000 --- a/Emby.Server.Implementations/SocketSharp/HttpFile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.IO; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class HttpFile : IHttpFile - { - public string Name { get; set; } - - public string FileName { get; set; } - - public long ContentLength { get; set; } - - public string ContentType { get; set; } - - public Stream InputStream { get; set; } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs b/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs deleted file mode 100644 index 7479d8104..000000000 --- a/Emby.Server.Implementations/SocketSharp/HttpPostedFile.cs +++ /dev/null @@ -1,198 +0,0 @@ -using System; -using System.IO; - -public sealed class HttpPostedFile : IDisposable -{ - private string _name; - private string _contentType; - private Stream _stream; - private bool _disposed = false; - - internal HttpPostedFile(string name, string content_type, Stream base_stream, long offset, long length) - { - _name = name; - _contentType = content_type; - _stream = new ReadSubStream(base_stream, offset, length); - } - - public string ContentType => _contentType; - - public int ContentLength => (int)_stream.Length; - - public string FileName => _name; - - public Stream InputStream => _stream; - - /// <summary> - /// Releases the unmanaged resources and disposes of the managed resources used. - /// </summary> - public void Dispose() - { - if (_disposed) - { - return; - } - - _stream.Dispose(); - _stream = null; - - _name = null; - _contentType = null; - - _disposed = true; - } - - private class ReadSubStream : Stream - { - private Stream _stream; - private long _offset; - private long _end; - private long _position; - - public ReadSubStream(Stream s, long offset, long length) - { - _stream = s; - _offset = offset; - _end = offset + length; - _position = offset; - } - - public override bool CanRead => true; - - public override bool CanSeek => true; - - public override bool CanWrite => false; - - public override long Length => _end - _offset; - - public override long Position - { - get => _position - _offset; - set - { - if (value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - - _position = Seek(value, SeekOrigin.Begin); - } - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int dest_offset, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (dest_offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(dest_offset), "< 0"); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "< 0"); - } - - int len = buffer.Length; - if (dest_offset > len) - { - throw new ArgumentException("destination offset is beyond array size", nameof(dest_offset)); - } - - // reordered to avoid possible integer overflow - if (dest_offset > len - count) - { - throw new ArgumentException("Reading would overrun buffer", nameof(count)); - } - - if (count > _end - _position) - { - count = (int)(_end - _position); - } - - if (count <= 0) - { - return 0; - } - - _stream.Position = _position; - int result = _stream.Read(buffer, dest_offset, count); - if (result > 0) - { - _position += result; - } - else - { - _position = _end; - } - - return result; - } - - public override int ReadByte() - { - if (_position >= _end) - { - return -1; - } - - _stream.Position = _position; - int result = _stream.ReadByte(); - if (result < 0) - { - _position = _end; - } - else - { - _position++; - } - - return result; - } - - public override long Seek(long d, SeekOrigin origin) - { - long real; - switch (origin) - { - case SeekOrigin.Begin: - real = _offset + d; - break; - case SeekOrigin.End: - real = _end + d; - break; - case SeekOrigin.Current: - real = _position + d; - break; - default: - throw new ArgumentException("Unknown SeekOrigin value", nameof(origin)); - } - - long virt = real - _offset; - if (virt < 0 || virt > Length) - { - throw new ArgumentException("Invalid position", nameof(d)); - } - - _position = _stream.Seek(real, SeekOrigin.Begin); - return _position; - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs index ee5131c1f..aa76901a4 100644 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; @@ -209,7 +211,7 @@ namespace Emby.Server.Implementations.SocketSharp private static string GetQueryStringContentType(HttpRequest httpReq) { ReadOnlySpan<char> format = httpReq.Query["format"].ToString(); - if (format == null) + if (format == ReadOnlySpan<char>.Empty) { const int FormatMaxLength = 4; ReadOnlySpan<char> pi = httpReq.Path.ToString(); diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 16507466f..67e31f7f6 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; diff --git a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs index 87d3ae2d6..980954ba0 100644 --- a/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -8,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting public class CommunityRatingComparer : IBaseItemComparer { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.CommunityRating; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -16,18 +24,16 @@ namespace Emby.Server.Implementations.Sorting public int Compare(BaseItem x, BaseItem y) { if (x == null) + { throw new ArgumentNullException(nameof(x)); + } if (y == null) + { throw new ArgumentNullException(nameof(y)); + } return (x.CommunityRating ?? 0).CompareTo(y.CommunityRating ?? 0); } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.CommunityRating; } } diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs index 623675157..5c1503ed2 100644 --- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs +++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -27,6 +29,12 @@ namespace Emby.Server.Implementations.Sorting public IUserDataManager UserDataRepository { get; set; } /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.DateLastContentAdded; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -44,9 +52,7 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private static DateTime GetDate(BaseItem x) { - var folder = x as Folder; - - if (folder != null) + if (x is Folder folder) { if (folder.DateLastMediaAdded.HasValue) { @@ -56,11 +62,5 @@ namespace Emby.Server.Implementations.Sorting return DateTime.MinValue; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.DateLastContentAdded; } } diff --git a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs index 66de05a6a..aba14c6ca 100644 --- a/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFavoriteOrLikeComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -14,6 +16,24 @@ namespace Emby.Server.Implementations.Sorting public User User { get; set; } /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IsFavoriteOrLiked; + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + public IUserDataManager UserDataRepository { get; set; } + + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + public IUserManager UserManager { get; set; } + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting { return x.IsFavoriteOrLiked(User) ? 0 : 1; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.IsFavoriteOrLiked; - - /// <summary> - /// Gets or sets the user data repository. - /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } - - /// <summary> - /// Gets or sets the user manager. - /// </summary> - /// <value>The user manager.</value> - public IUserManager UserManager { get; set; } } } diff --git a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs index dfaa144cd..a35192eff 100644 --- a/Emby.Server.Implementations/Sorting/IsFolderComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsFolderComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Querying; @@ -7,6 +9,12 @@ namespace Emby.Server.Implementations.Sorting public class IsFolderComparer : IBaseItemComparer { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IsFolder; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -26,11 +34,5 @@ namespace Emby.Server.Implementations.Sorting { return x.IsFolder ? 0 : 1; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.IsFolder; } } diff --git a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs index da3f3dd25..39d9bc68e 100644 --- a/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsPlayedComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -14,6 +16,24 @@ namespace Emby.Server.Implementations.Sorting public User User { get; set; } /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IsUnplayed; + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + public IUserDataManager UserDataRepository { get; set; } + + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + public IUserManager UserManager { get; set; } + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting { return x.IsPlayed(User) ? 0 : 1; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.IsUnplayed; - - /// <summary> - /// Gets or sets the user data repository. - /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } - - /// <summary> - /// Gets or sets the user manager. - /// </summary> - /// <value>The user manager.</value> - public IUserManager UserManager { get; set; } } } diff --git a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs index d99d0eff2..478df4035 100644 --- a/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs +++ b/Emby.Server.Implementations/Sorting/IsUnplayedComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Sorting; @@ -14,6 +16,24 @@ namespace Emby.Server.Implementations.Sorting public User User { get; set; } /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IsUnplayed; + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + public IUserDataManager UserDataRepository { get; set; } + + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + public IUserManager UserManager { get; set; } + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -33,23 +53,5 @@ namespace Emby.Server.Implementations.Sorting { return x.IsUnplayed(User) ? 0 : 1; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.IsUnplayed; - - /// <summary> - /// Gets or sets the user data repository. - /// </summary> - /// <value>The user data repository.</value> - public IUserDataManager UserDataRepository { get; set; } - - /// <summary> - /// Gets or sets the user manager. - /// </summary> - /// <value>The user manager.</value> - public IUserManager UserManager { get; set; } } } diff --git a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs index 7afbd9ff7..76bb798b5 100644 --- a/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs +++ b/Emby.Server.Implementations/Sorting/OfficialRatingComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -16,6 +18,12 @@ namespace Emby.Server.Implementations.Sorting } /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.OfficialRating; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -38,11 +46,5 @@ namespace Emby.Server.Implementations.Sorting return levelX.CompareTo(levelY); } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.OfficialRating; } } diff --git a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs index 504b6d283..b9205ee07 100644 --- a/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs +++ b/Emby.Server.Implementations/Sorting/SeriesSortNameComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Sorting; @@ -8,6 +10,12 @@ namespace Emby.Server.Implementations.Sorting public class SeriesSortNameComparer : IBaseItemComparer { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.SeriesSortName; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -18,12 +26,6 @@ namespace Emby.Server.Implementations.Sorting return string.Compare(GetValue(x), GetValue(y), StringComparison.CurrentCultureIgnoreCase); } - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.SeriesSortName; - private static string GetValue(BaseItem item) { var hasSeries = item as IHasSeries; diff --git a/Emby.Server.Implementations/Sorting/StartDateComparer.cs b/Emby.Server.Implementations/Sorting/StartDateComparer.cs index aa040fa15..558a3d351 100644 --- a/Emby.Server.Implementations/Sorting/StartDateComparer.cs +++ b/Emby.Server.Implementations/Sorting/StartDateComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.LiveTv; @@ -9,6 +11,12 @@ namespace Emby.Server.Implementations.Sorting public class StartDateComparer : IBaseItemComparer { /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.StartDate; + + /// <summary> /// Compares the specified x. /// </summary> /// <param name="x">The x.</param> @@ -26,19 +34,12 @@ namespace Emby.Server.Implementations.Sorting /// <returns>DateTime.</returns> private static DateTime GetDate(BaseItem x) { - var hasStartDate = x as LiveTvProgram; - - if (hasStartDate != null) + if (x is LiveTvProgram hasStartDate) { return hasStartDate.StartDate; } + return DateTime.MinValue; } - - /// <summary> - /// Gets the name. - /// </summary> - /// <value>The name.</value> - public string Name => ItemSortBy.StartDate; } } diff --git a/Emby.Server.Implementations/Sorting/StudioComparer.cs b/Emby.Server.Implementations/Sorting/StudioComparer.cs index c9ac765c1..5766dc542 100644 --- a/Emby.Server.Implementations/Sorting/StudioComparer.cs +++ b/Emby.Server.Implementations/Sorting/StudioComparer.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Linq; using MediaBrowser.Controller.Entities; diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs new file mode 100644 index 000000000..d0812a13f --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -0,0 +1,522 @@ +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(); + + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayController" /> class. + /// </summary> + /// <param name="sessionManager">The session manager.</param> + /// <param name="syncPlayManager">The SyncPlay manager.</param> + 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..129262e53 --- /dev/null +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayManager.cs @@ -0,0 +1,405 @@ +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; + + /// <summary> + /// Initializes a new instance of the <see cref="SyncPlayManager" /> class. + /// </summary> + /// <param name="logger">The logger.</param> + /// <param name="userManager">The user manager.</param> + /// <param name="sessionManager">The session manager.</param> + /// <param name="libraryManager">The library manager.</param> + 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/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index 4c2f24e6f..383615f74 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -1,8 +1,9 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; using System.Linq; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; @@ -18,14 +19,12 @@ namespace Emby.Server.Implementations.TV private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; private readonly ILibraryManager _libraryManager; - private readonly IServerConfigurationManager _config; - public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager config) + public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager) { _userManager = userManager; _userDataManager = userDataManager; _libraryManager = libraryManager; - _config = config; } public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions) 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 684fe5672..178f32c31 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -1,12 +1,12 @@ +#pragma warning disable CS1591 + using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Net.Http; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Security.Cryptography; using System.Threading; diff --git a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs index a3f3f6cb4..7b7d66ca6 100644 --- a/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/CollectionFolderImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; diff --git a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs index 78ac95f85..e7888595f 100644 --- a/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/DynamicImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.IO; @@ -19,13 +21,11 @@ namespace Emby.Server.Implementations.UserViews public class DynamicImageProvider : BaseDynamicImageProvider<UserView> { private readonly IUserManager _userManager; - private readonly ILibraryManager _libraryManager; - public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager, ILibraryManager libraryManager) + public DynamicImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, IUserManager userManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) { _userManager = userManager; - _libraryManager = libraryManager; } protected override IReadOnlyList<BaseItem> GetItemsWithImages(BaseItem item) diff --git a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs index 4655cd928..58a023638 100644 --- a/Emby.Server.Implementations/UserViews/FolderImageProvider.cs +++ b/Emby.Server.Implementations/UserViews/FolderImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System.Collections.Generic; using Emby.Server.Implementations.Images; using MediaBrowser.Common.Configuration; @@ -75,14 +77,14 @@ namespace Emby.Server.Implementations.UserViews return false; } - var folder = item as Folder; - if (folder != null) + if (item is Folder folder) { if (folder.IsTopParent) { return false; } } + return true; //return item.SourceType == SourceType.Library; } |
