From 974a04c12939068b23b62ee6ebb1e7fc2e830eec Mon Sep 17 00:00:00 2001 From: dkanada Date: Wed, 26 Feb 2020 01:58:39 +0900 Subject: [PATCH 01/97] update plugin classes for nightly builds --- .../Activity/ActivityLogEntryPoint.cs | 8 +- .../ApplicationHost.cs | 10 +- .../Updates/InstallationManager.cs | 68 +++++------ MediaBrowser.Api/PackageService.cs | 36 +----- .../Extensions/BaseExtensions.cs | 1 - MediaBrowser.Common/IApplicationHost.cs | 4 +- MediaBrowser.Common/Plugins/BasePlugin.cs | 8 +- .../Updates/IInstallationManager.cs | 22 ++-- .../Updates/InstallationEventArgs.cs | 2 +- MediaBrowser.Model/MediaBrowser.Model.csproj | 2 +- MediaBrowser.Model/System/SystemInfo.cs | 2 +- .../Updates/CheckForUpdateResult.cs | 4 +- .../Updates/InstallationInfo.cs | 2 +- MediaBrowser.Model/Updates/PackageInfo.cs | 106 +----------------- .../Updates/PackageTargetSystem.cs | 23 ---- .../Updates/PackageVersionInfo.cs | 97 ---------------- ...ckageVersionClass.cs => ReleaseChannel.cs} | 15 +-- MediaBrowser.Model/Updates/VersionInfo.cs | 73 ++++++++++++ 18 files changed, 150 insertions(+), 333 deletions(-) delete mode 100644 MediaBrowser.Model/Updates/PackageTargetSystem.cs delete mode 100644 MediaBrowser.Model/Updates/PackageVersionInfo.cs rename MediaBrowser.Model/Updates/{PackageVersionClass.cs => ReleaseChannel.cs} (51%) create mode 100644 MediaBrowser.Model/Updates/VersionInfo.cs diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index ac8af66a20..0f0b8b97b1 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -421,7 +421,7 @@ namespace Emby.Server.Implementations.Activity }); } - private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e) + private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, VersionInfo)> e) { CreateLogEntry(new ActivityLogEntry { @@ -433,7 +433,7 @@ namespace Emby.Server.Implementations.Activity ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.Item2.versionStr), + e.Argument.Item2.versionString), Overview = e.Argument.Item2.description }); } @@ -450,7 +450,7 @@ namespace Emby.Server.Implementations.Activity }); } - private void OnPluginInstalled(object sender, GenericEventArgs e) + private void OnPluginInstalled(object sender, GenericEventArgs e) { CreateLogEntry(new ActivityLogEntry { @@ -462,7 +462,7 @@ namespace Emby.Server.Implementations.Activity ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.versionStr) + e.Argument.versionString) }); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dee0edd26e..ad77ab8b48 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -212,14 +212,14 @@ namespace Emby.Server.Implementations public IFileSystem FileSystemManager { get; set; } /// - public PackageVersionClass SystemUpdateLevel + public ReleaseChannel SystemUpdateLevel { get { -#if BETA - return PackageVersionClass.Beta; +#if NIGHTLY + return PackageChannel.Nightly; #else - return PackageVersionClass.Release; + return ReleaseChannel.Stable; #endif } } @@ -1003,7 +1003,7 @@ namespace Emby.Server.Implementations AuthenticatedAttribute.AuthService = AuthService; } - private async void PluginInstalled(object sender, GenericEventArgs args) + private async void PluginInstalled(object sender, GenericEventArgs args) { string dir = Path.Combine(ApplicationPaths.PluginsPath, args.Argument.name); var types = Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index c897036eb8..1450c74d2d 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -23,12 +23,12 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Updates { /// - /// Manages all install, uninstall and update operations (both plugins and system). + /// Manages all install, uninstall, and update operations for the system and individual plugins. /// public class InstallationManager : IInstallationManager { /// - /// The _logger. + /// The logger. /// private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; @@ -101,10 +101,10 @@ namespace Emby.Server.Implementations.Updates public event EventHandler> PluginUninstalled; /// - public event EventHandler> PluginUpdated; + public event EventHandler> PluginUpdated; /// - public event EventHandler> PluginInstalled; + public event EventHandler> PluginInstalled; /// public IEnumerable CompletedInstallations => _completedInstallationsInternal; @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.Updates using (var response = await _httpClient.SendAsync( new HttpRequestOptions { - Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", + Url = "https://repo.jellyfin.org/releases/plugin/manifest-water.json", CancellationToken = cancellationToken, CacheMode = CacheMode.Unconditional, CacheLength = TimeSpan.FromMinutes(3) @@ -148,48 +148,48 @@ namespace Emby.Server.Implementations.Updates } /// - public IEnumerable GetCompatibleVersions( - IEnumerable availableVersions, + public IEnumerable GetCompatibleVersions( + IEnumerable availableVersions, Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release) + ReleaseChannel releaseChannel = ReleaseChannel.Stable) { var appVer = _applicationHost.ApplicationVersion; availableVersions = availableVersions - .Where(x => x.classification == classification - && Version.Parse(x.requiredVersionStr) <= appVer); + .Where(x => x.channel == releaseChannel + && Version.Parse(x.minimumServerVersion) <= appVer); if (minVersion != null) { - availableVersions = availableVersions.Where(x => x.Version >= minVersion); + availableVersions = availableVersions.Where(x => x.versionCode >= minVersion); } - return availableVersions.OrderByDescending(x => x.Version); + return availableVersions.OrderByDescending(x => x.versionCode); } /// - public IEnumerable GetCompatibleVersions( + public IEnumerable GetCompatibleVersions( IEnumerable availablePackages, string name = null, Guid guid = default, Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release) + ReleaseChannel releaseChannel = ReleaseChannel.Stable) { var package = FilterPackages(availablePackages, name, guid).FirstOrDefault(); - // Package not found. + // Package not found in repository if (package == null) { - return Enumerable.Empty(); + return Enumerable.Empty(); } return GetCompatibleVersions( package.versions, minVersion, - classification); + releaseChannel); } /// - public async IAsyncEnumerable GetAvailablePluginUpdates([EnumeratorCancellation] CancellationToken cancellationToken = default) + public async IAsyncEnumerable GetAvailablePluginUpdates([EnumeratorCancellation] CancellationToken cancellationToken = default) { var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false); @@ -198,8 +198,8 @@ namespace Emby.Server.Implementations.Updates // Figure out what needs to be installed foreach (var plugin in _applicationHost.Plugins) { - var compatibleversions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version, systemUpdateLevel); - var version = compatibleversions.FirstOrDefault(y => y.Version > plugin.Version); + var compatibleVersions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version, systemUpdateLevel); + var version = compatibleVersions.FirstOrDefault(y => y.versionCode > plugin.Version); if (version != null && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase))) { @@ -209,7 +209,7 @@ namespace Emby.Server.Implementations.Updates } /// - public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken) + public async Task InstallPackage(VersionInfo package, CancellationToken cancellationToken) { if (package == null) { @@ -221,8 +221,8 @@ namespace Emby.Server.Implementations.Updates Id = Guid.NewGuid(), Name = package.name, AssemblyGuid = package.guid, - UpdateClass = package.classification, - Version = package.versionStr + UpdateClass = package.channel, + Version = package.versionString }; var innerCancellationTokenSource = new CancellationTokenSource(); @@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.Updates var installationEventArgs = new InstallationEventArgs { InstallationInfo = installationInfo, - PackageVersionInfo = package + VersionInfo = package }; PackageInstalling?.Invoke(this, installationEventArgs); @@ -265,7 +265,7 @@ namespace Emby.Server.Implementations.Updates _currentInstallations.Remove(tuple); } - _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr); + _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionString); PackageInstallationCancelled?.Invoke(this, installationEventArgs); @@ -301,7 +301,7 @@ namespace Emby.Server.Implementations.Updates /// The package. /// The cancellation token. /// . - private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken) + private async Task InstallPackageInternal(VersionInfo package, CancellationToken cancellationToken) { // Set last update time if we were installed before IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) @@ -313,26 +313,26 @@ namespace Emby.Server.Implementations.Updates // Do plugin-specific processing if (plugin == null) { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionString ?? string.Empty, package.channel); - PluginInstalled?.Invoke(this, new GenericEventArgs(package)); + PluginInstalled?.Invoke(this, new GenericEventArgs(package)); } else { - _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionString ?? string.Empty, package.channel); - PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package))); + PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package))); } _applicationHost.NotifyPendingRestart(); } - private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(VersionInfo package, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.targetFilename); + var extension = Path.GetExtension(package.filename); if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) { - _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename); + _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.filename); return; } @@ -379,7 +379,7 @@ namespace Emby.Server.Implementations.Updates } /// - /// Uninstalls a plugin + /// Uninstalls a plugin. /// /// The plugin. public void UninstallPlugin(IPlugin plugin) diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index afc3e026a8..ccc978295c 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -42,23 +42,6 @@ namespace MediaBrowser.Api [Authenticated] public class GetPackages : IReturn { - /// - /// Gets or sets the name. - /// - /// The name. - [ApiMember(Name = "PackageType", Description = "Optional package type filter (System/UserInstalled)", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] - public string PackageType { get; set; } - - [ApiMember(Name = "TargetSystems", Description = "Optional. Filter by target system type. Allows multiple, comma delimited.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET", AllowMultiple = true)] - public string TargetSystems { get; set; } - - [ApiMember(Name = "IsPremium", Description = "Optional. Filter by premium status", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? IsPremium { get; set; } - - [ApiMember(Name = "IsAdult", Description = "Optional. Filter by package that contain adult content.", IsRequired = false, DataType = "boolean", ParameterType = "query", Verb = "GET")] - public bool? IsAdult { get; set; } - - public bool? IsAppStoreEnabled { get; set; } } /// @@ -94,7 +77,7 @@ namespace MediaBrowser.Api /// /// The update class. [ApiMember(Name = "UpdateClass", Description = "Optional update class (Dev, Beta, Release). Defaults to Release.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public PackageVersionClass UpdateClass { get; set; } + public ReleaseChannel UpdateClass { get; set; } } /// @@ -154,23 +137,6 @@ namespace MediaBrowser.Api { IEnumerable packages = await _installationManager.GetAvailablePackages().ConfigureAwait(false); - if (!string.IsNullOrEmpty(request.TargetSystems)) - { - var apps = request.TargetSystems.Split(',').Select(i => (PackageTargetSystem)Enum.Parse(typeof(PackageTargetSystem), i, true)); - - packages = packages.Where(p => apps.Contains(p.targetSystem)); - } - - if (request.IsAdult.HasValue) - { - packages = packages.Where(p => p.adult == request.IsAdult.Value); - } - - if (request.IsAppStoreEnabled.HasValue) - { - packages = packages.Where(p => p.enableInAppStore == request.IsAppStoreEnabled.Value); - } - return ToOptimizedResult(packages.ToArray()); } diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index 08964420e7..bc002e5233 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -35,7 +35,6 @@ namespace MediaBrowser.Common.Extensions { return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str))); } - #pragma warning restore CA5351 } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 68a24aabaa..c88eac27a1 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -50,8 +50,8 @@ namespace MediaBrowser.Common /// /// Gets the version class of the system. /// - /// or . - PackageVersionClass SystemUpdateLevel { get; } + /// or . + ReleaseChannel SystemUpdateLevel { get; } /// /// Gets the application version. diff --git a/MediaBrowser.Common/Plugins/BasePlugin.cs b/MediaBrowser.Common/Plugins/BasePlugin.cs index b24d10ff10..9e4a360c38 100644 --- a/MediaBrowser.Common/Plugins/BasePlugin.cs +++ b/MediaBrowser.Common/Plugins/BasePlugin.cs @@ -67,7 +67,7 @@ namespace MediaBrowser.Common.Plugins } /// - /// Called when just before the plugin is uninstalled from the server. + /// Called just before the plugin is uninstalled from the server. /// public virtual void OnUninstalling() { @@ -101,7 +101,7 @@ namespace MediaBrowser.Common.Plugins private readonly object _configurationSyncLock = new object(); /// - /// The save lock. + /// The configuration save lock. /// private readonly object _configurationSaveLock = new object(); @@ -148,7 +148,7 @@ namespace MediaBrowser.Common.Plugins protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath); /// - /// Gets or sets the plugin's configuration. + /// Gets or sets the plugin configuration. /// /// The configuration. public TConfigurationType Configuration @@ -186,7 +186,7 @@ namespace MediaBrowser.Common.Plugins public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName); /// - /// Gets the plugin's configuration. + /// Gets the plugin configuration. /// /// The configuration. BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration; diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index a09c1916c5..284e418d9d 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -29,12 +29,12 @@ namespace MediaBrowser.Common.Updates /// /// Occurs when a plugin is updated. /// - event EventHandler> PluginUpdated; + event EventHandler> PluginUpdated; /// /// Occurs when a plugin is installed. /// - event EventHandler> PluginInstalled; + event EventHandler> PluginInstalled; /// /// Gets the completed installations. @@ -65,12 +65,12 @@ namespace MediaBrowser.Common.Updates /// /// The available version of the plugin. /// The minimum required version of the plugin. - /// The classification of updates. + /// The classification of updates. /// All compatible versions ordered from newest to oldest. - IEnumerable GetCompatibleVersions( - IEnumerable availableVersions, + IEnumerable GetCompatibleVersions( + IEnumerable availableVersions, Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release); + ReleaseChannel releaseChannel = ReleaseChannel.Stable); /// /// Returns all compatible versions ordered from newest to oldest. @@ -79,21 +79,21 @@ namespace MediaBrowser.Common.Updates /// The name. /// The guid of the plugin. /// The minimum required version of the plugin. - /// The classification. + /// The classification. /// All compatible versions ordered from newest to oldest. - IEnumerable GetCompatibleVersions( + IEnumerable GetCompatibleVersions( IEnumerable availablePackages, string name = null, Guid guid = default, Version minVersion = null, - PackageVersionClass classification = PackageVersionClass.Release); + ReleaseChannel releaseChannel = ReleaseChannel.Stable); /// /// Returns the available plugin updates. /// /// The cancellation token. /// The available plugin updates. - IAsyncEnumerable GetAvailablePluginUpdates(CancellationToken cancellationToken = default); + IAsyncEnumerable GetAvailablePluginUpdates(CancellationToken cancellationToken = default); /// /// Installs the package. @@ -101,7 +101,7 @@ namespace MediaBrowser.Common.Updates /// The package. /// The cancellation token. /// . - Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default); + Task InstallPackage(VersionInfo package, CancellationToken cancellationToken = default); /// /// Uninstalls a plugin. diff --git a/MediaBrowser.Common/Updates/InstallationEventArgs.cs b/MediaBrowser.Common/Updates/InstallationEventArgs.cs index 8bbb231ce1..f459fd8256 100644 --- a/MediaBrowser.Common/Updates/InstallationEventArgs.cs +++ b/MediaBrowser.Common/Updates/InstallationEventArgs.cs @@ -9,6 +9,6 @@ namespace MediaBrowser.Common.Updates { public InstallationInfo InstallationInfo { get; set; } - public PackageVersionInfo PackageVersionInfo { get; set; } + public VersionInfo VersionInfo { get; set; } } } diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 6576657662..41644ad334 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -11,7 +11,7 @@ netstandard2.1 false true - true + true diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index 190411c9ba..da39ee208a 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -27,7 +27,7 @@ namespace MediaBrowser.Model.System /// public class SystemInfo : PublicSystemInfo { - public PackageVersionClass SystemUpdateLevel { get; set; } + public ReleaseChannel SystemUpdateLevel { get; set; } /// /// Gets or sets the display name of the operating system. diff --git a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs index be1b082238..883fc636b4 100644 --- a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs +++ b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs @@ -17,13 +17,13 @@ namespace MediaBrowser.Model.Updates /// The available version. public string AvailableVersion { - get => Package != null ? Package.versionStr : "0.0.0.1"; + get => Package != null ? Package.versionString : "0.0.0.1"; set { } // need this for the serializer } /// /// Get or sets package information for an available update /// - public PackageVersionInfo Package { get; set; } + public VersionInfo Package { get; set; } } } diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index 42c2105f54..870bf8c0be 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -35,6 +35,6 @@ namespace MediaBrowser.Model.Updates /// Gets or sets the update class. /// /// The update class. - public PackageVersionClass UpdateClass { get; set; } + public ReleaseChannel UpdateClass { get; set; } } } diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index abbe91eff6..d06ffe1e6c 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -8,12 +8,6 @@ namespace MediaBrowser.Model.Updates /// public class PackageInfo { - /// - /// The internal id of this package. - /// - /// The id. - public string id { get; set; } - /// /// Gets or sets the name. /// @@ -32,24 +26,6 @@ namespace MediaBrowser.Model.Updates /// The overview. public string overview { get; set; } - /// - /// Gets or sets a value indicating whether this instance is premium. - /// - /// true if this instance is premium; otherwise, false. - public bool isPremium { get; set; } - - /// - /// Gets or sets a value indicating whether this instance is adult only content. - /// - /// true if this instance is adult; otherwise, false. - public bool adult { get; set; } - - /// - /// Gets or sets the rich desc URL. - /// - /// The rich desc URL. - public string richDescUrl { get; set; } - /// /// Gets or sets the thumb image. /// @@ -63,16 +39,10 @@ namespace MediaBrowser.Model.Updates public string previewImage { get; set; } /// - /// Gets or sets the type. - /// - /// The type. - public string type { get; set; } - - /// - /// Gets or sets the target filename. + /// Gets or sets the target filename for the downloaded binary. /// /// The target filename. - public string targetFilename { get; set; } + public string filename { get; set; } /// /// Gets or sets the owner. @@ -87,90 +57,24 @@ namespace MediaBrowser.Model.Updates public string category { get; set; } /// - /// Gets or sets the catalog tile color. - /// - /// The owner. - public string tileColor { get; set; } - - /// - /// Gets or sets the feature id of this package (if premium). - /// - /// The feature id. - public string featureId { get; set; } - - /// - /// Gets or sets the registration info for this package (if premium). - /// - /// The registration info. - public string regInfo { get; set; } - - /// - /// Gets or sets the price for this package (if premium). - /// - /// The price. - public float price { get; set; } - - /// - /// Gets or sets the target system for this plug-in (Server, MBTheater, MBClassic). - /// - /// The target system. - public PackageTargetSystem targetSystem { get; set; } - - /// - /// The guid of the assembly associated with this package (if a plug-in). + /// The guid of the assembly associated with this plugin. /// This is used to identify the proper item for automatic updates. /// /// The name. public string guid { get; set; } - /// - /// Gets or sets the total number of ratings for this package. - /// - /// The total ratings. - public int? totalRatings { get; set; } - - /// - /// Gets or sets the average rating for this package . - /// - /// The rating. - public float avgRating { get; set; } - - /// - /// Gets or sets whether or not this package is registered. - /// - /// True if registered. - public bool isRegistered { get; set; } - - /// - /// Gets or sets the expiration date for this package. - /// - /// Expiration Date. - public DateTime expDate { get; set; } - /// /// Gets or sets the versions. /// /// The versions. - public IReadOnlyList versions { get; set; } - - /// - /// Gets or sets a value indicating whether [enable in application store]. - /// - /// true if [enable in application store]; otherwise, false. - public bool enableInAppStore { get; set; } - - /// - /// Gets or sets the installs. - /// - /// The installs. - public int installs { get; set; } + public IReadOnlyList versions { get; set; } /// /// Initializes a new instance of the class. /// public PackageInfo() { - versions = Array.Empty(); + versions = Array.Empty(); } } } diff --git a/MediaBrowser.Model/Updates/PackageTargetSystem.cs b/MediaBrowser.Model/Updates/PackageTargetSystem.cs deleted file mode 100644 index 11af7f02dd..0000000000 --- a/MediaBrowser.Model/Updates/PackageTargetSystem.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// - /// Enum PackageType. - /// - public enum PackageTargetSystem - { - /// - /// Server. - /// - Server, - - /// - /// MB Theater. - /// - MBTheater, - - /// - /// MB Classic. - /// - MBClassic - } -} diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs deleted file mode 100644 index 85d8fde860..0000000000 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ /dev/null @@ -1,97 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1600 - -using System; -using System.Text.Json.Serialization; - -namespace MediaBrowser.Model.Updates -{ - /// - /// Class PackageVersionInfo. - /// - public class PackageVersionInfo - { - /// - /// Gets or sets the name. - /// - /// The name. - public string name { get; set; } - - /// - /// Gets or sets the guid. - /// - /// The guid. - public string guid { get; set; } - - /// - /// Gets or sets the version STR. - /// - /// The version STR. - public string versionStr { get; set; } - - /// - /// The _version - /// - private Version _version; - - /// - /// Gets or sets the version. - /// Had to make this an interpreted property since Protobuf can't handle Version - /// - /// The version. - [JsonIgnore] - public Version Version - { - get - { - if (_version == null) - { - var ver = versionStr; - _version = new Version(string.IsNullOrEmpty(ver) ? "0.0.0.1" : ver); - } - - return _version; - } - } - - /// - /// Gets or sets the classification. - /// - /// The classification. - public PackageVersionClass classification { get; set; } - - /// - /// Gets or sets the description. - /// - /// The description. - public string description { get; set; } - - /// - /// Gets or sets the required version STR. - /// - /// The required version STR. - public string requiredVersionStr { get; set; } - - /// - /// Gets or sets the source URL. - /// - /// The source URL. - public string sourceUrl { get; set; } - - /// - /// Gets or sets the source URL. - /// - /// The source URL. - public string checksum { get; set; } - - /// - /// Gets or sets the target filename. - /// - /// The target filename. - public string targetFilename { get; set; } - - public string infoUrl { get; set; } - - public string runtimes { get; set; } - } -} diff --git a/MediaBrowser.Model/Updates/PackageVersionClass.cs b/MediaBrowser.Model/Updates/ReleaseChannel.cs similarity index 51% rename from MediaBrowser.Model/Updates/PackageVersionClass.cs rename to MediaBrowser.Model/Updates/ReleaseChannel.cs index f813f2c974..ed4a774a72 100644 --- a/MediaBrowser.Model/Updates/PackageVersionClass.cs +++ b/MediaBrowser.Model/Updates/ReleaseChannel.cs @@ -3,21 +3,16 @@ namespace MediaBrowser.Model.Updates /// /// Enum PackageVersionClass. /// - public enum PackageVersionClass + public enum ReleaseChannel { /// - /// The release. + /// The stable. /// - Release = 0, + Stable = 0, /// - /// The beta. + /// The nightly. /// - Beta = 1, - - /// - /// The dev. - /// - Dev = 2 + Nightly = 1 } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs new file mode 100644 index 0000000000..ad893db2e2 --- /dev/null +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -0,0 +1,73 @@ +#pragma warning disable CS1591 +#pragma warning disable SA1600 + +using System; + +namespace MediaBrowser.Model.Updates +{ + /// + /// Class PackageVersionInfo. + /// + public class VersionInfo + { + /// + /// Gets or sets the name. + /// + /// The name. + public string name { get; set; } + + /// + /// Gets or sets the guid. + /// + /// The guid. + public string guid { get; set; } + + /// + /// Gets or sets the version string. + /// + /// The version string. + public string versionString { get; set; } + + /// + /// Gets or sets the version. + /// + /// The version. + public Version versionCode { get; set; } + + /// + /// Gets or sets the release channel. + /// + /// The release channel for a given package version. + public ReleaseChannel channel { get; set; } + + /// + /// Gets or sets the description. + /// + /// The description. + public string description { get; set; } + + /// + /// Gets or sets the minimum required version for the server. + /// + /// The minimum required version. + public string minimumServerVersion { get; set; } + + /// + /// Gets or sets the source URL. + /// + /// The source URL. + public string sourceUrl { get; set; } + + /// + /// Gets or sets a checksum for the binary. + /// + /// The checksum. + public string checksum { get; set; } + + /// + /// Gets or sets the target filename for the downloaded binary. + /// + /// The target filename. + public string filename { get; set; } + } +} From 681dd8d32fbb4fdb67a4b82125179bd40d03edbd Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 17 Mar 2020 14:21:00 +0100 Subject: [PATCH 02/97] Add recommended extensions to VS Code configuration --- .vscode/extensions.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .vscode/extensions.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000..59d9452fed --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,14 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + "ms-dotnettools.csharp", + "editorconfig.editorconfig" + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} From 751dff09dc6f57be14f071346a79a23f33fa48c5 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 17 Mar 2020 14:21:24 +0100 Subject: [PATCH 03/97] Add development instructions to README with details on running from source --- README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/README.md b/README.md index ea54b8c8b0..74042d8d83 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,92 @@ Most of the translations can be found in the web client but we have several othe Detailed Translation Status + +## Development + +These instructions will help you get set up with a local development environment in order to contribute to this repository. Before you start, please be sure to completely read our [guidelines on development contributions](https://jellyfin.org/docs/general/contributing/development.html). Note that this project is supported on all major operating systems (Windows, Mac and Linux). + +### Prerequisites + +The following software prerequisites are required to be installed locally before the project can be built and executed. + +* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) +* [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least version 2017) or [Visual Studio Code](https://code.visualstudio.com/Download) + +### Cloning the Repository + +After dependencies are installed you will need to clone a local copy of this repository. If you just want to run the server from source you can clone this repository directly, but if you are intending to contribute code changes to the project, you should [set up your own fork](https://jellyfin.org/docs/general/contributing/development.html#set-up-your-copy-of-the-repo) of the repository. The following example shows how you can clone the repository directly over HTTPS. + +```bash +git clone https://github.com/jellyfin/jellyfin.git +``` + +### Installing the Web Client + +By default, the server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend API. before you can run the server, you will need to get a copy of the web client files since they are not included in this repository directly. + +Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. + +There are two options to get the files for the web client: + +1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) +2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located here: `C:\Program Files\Jellyfin\Server\jellyfin-web` + +Once you have a copy of the built web client files, you need to copy them into the build output directory of the web server project. For example: `\Jellyfin.Server\bin\Debug\netcoreapp3.1\jellyfin-web` + +### Running The Server + +The following instructions will help you get the project up and running via the command line, or your preferred IDE. + +#### Running With Visual Studio + +To run the project with Visual Studio you can open the Solution (`.sln`) file and then press `F5` to run the server. + +#### Running With Visual Studio Code + +To run the project with Visual Studio Code you will first need to open the repository directory with Visual Studio Code using the `Open Folder...` option. + +Second, you need to [install the recommended extensions for the workspace](https://code.visualstudio.com/docs/editor/extension-gallery#_recommended-extensions). Note that extension recommendations are classified as either "Workspace Recommendations" or "Other Recommendations", but only the "Workspace Recommendations" are required. + +After the required extensions are installed, you can can run the server by pressing `F5`. + +#### Running From The Command Line + +To run the server from the command line you can use the `dotnet run` command. The example below shows how to do this if you have cloned the repository into a directory named `jellyfin` (the default directory name) and should work on all operating systems. + +```bash +cd jellyfin # Move into the repository directory +cd Jellyfin.Server # Move into the server startup project directory +dotnet run # Run the server startup project +``` + +A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options. + +1. Build the project + + ```bash + dotnet build # Build the project + cd bin/Debug/netcoreapp3.1 # Change into the build output directory + ``` + +2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. + +### Running The Tests + +This repository also includes several unit test projects that are used to validate functionality as part of a CI process. These are several ways to run these tests: + +1. Run tests from the command line using `dotnet test` +2. Run tests in Visual Studio using the [Test Explorer](https://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer) +3. Run individual tests in Visual Studio Code using the associated [CodeLens annotation](https://github.com/OmniSharp/omnisharp-vscode/wiki/How-to-run-and-debug-unit-tests) + +### Advanced Configuration + +The following sections describe some more advanced scenarios for running the server from source that build upon the standard instructions above. + +#### Hosting The Web Client Separately + +It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for front-end developers who would prefer to host the client in a separate webpack development server for a tighter development loop (see the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this). + +To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. + +Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar. From 48f33f9a9669d0a237c85278a0cac3d3240a7c49 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 20 Mar 2020 12:21:20 +0100 Subject: [PATCH 04/97] Reword prerequisite section so that IDEs are listed as optional --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74042d8d83..d7f2da7900 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,9 @@ These instructions will help you get set up with a local development environment ### Prerequisites -The following software prerequisites are required to be installed locally before the project can be built and executed. +Before the the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) on your system. -* [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) -* [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least version 2017) or [Visual Studio Code](https://code.visualstudio.com/Download) +Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but explicit instructions for [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download) are included here. ### Cloning the Repository From cd34115e9981185e6a4f286d81f10160990e3f7c Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 20 Mar 2020 12:35:01 +0100 Subject: [PATCH 05/97] Remove duplicate text Co-Authored-By: artiume --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d7f2da7900..95659e8a8c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ These instructions will help you get set up with a local development environment ### Prerequisites -Before the the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) on your system. +Before the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) on your system. Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but explicit instructions for [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download) are included here. From 3fd245ba8789e862a67f41d5c481ccab6a49fb33 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sat, 21 Mar 2020 23:03:00 +0100 Subject: [PATCH 06/97] Add instructions for serving content over HTTPS --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 95659e8a8c..7b08723d6a 100644 --- a/README.md +++ b/README.md @@ -151,3 +151,9 @@ It is not necessary to host the frontend web client as part of the backend serve To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar. + +#### Serving Over HTTPS + +The .NET Core SDK includes a certificate that can be used to serve content over HTTPS while developing. When running from Visual Studio, VS Code, or using `dotnet run`, this behavior is automatically enabled by setting the environment variable `ASPNETCORE_ENVIRONMENT=Development` and you can access the HTTPS version of the site at https://localhost:8920. + +By default, the development certificate is not trusted so you will see a security warning when you browse to the site over HTTPS. On most browsers you can easily bypass this warning and continue to the site. However, if you want to get rid of the warning, you can configure your machine to trust the development certificate by following the instructions in the [ASP.NET Core documentation](https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos). From 499deb4fe591731f4c8542cf8afa8f93e7e68f8a Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 3 Apr 2020 20:05:58 -0400 Subject: [PATCH 07/97] Add section describing the repository with a link to contribution docs --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b08723d6a..2e8687bdc3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,11 @@ Most of the translations can be found in the web client but we have several othe Detailed Translation Status -## Development +## Jellyfin Server + +This repository contains the code for Jellyfin's back-end server. Note that this is only one of many projects/repositories under the Jellyfin GitHub [organization](https://github.com/jellyfin/). If you want to contribute, can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on. + +## Server Development These instructions will help you get set up with a local development environment in order to contribute to this repository. Before you start, please be sure to completely read our [guidelines on development contributions](https://jellyfin.org/docs/general/contributing/development.html). Note that this project is supported on all major operating systems (Windows, Mac and Linux). From 7ced986e0b73d45b243ede518f9166464af3de0d Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Fri, 3 Apr 2020 20:06:53 -0400 Subject: [PATCH 08/97] Remove section on serving over HTTPS This functionality has not been merged yet --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 2e8687bdc3..de5ea3f301 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,3 @@ It is not necessary to host the frontend web client as part of the backend serve To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar. - -#### Serving Over HTTPS - -The .NET Core SDK includes a certificate that can be used to serve content over HTTPS while developing. When running from Visual Studio, VS Code, or using `dotnet run`, this behavior is automatically enabled by setting the environment variable `ASPNETCORE_ENVIRONMENT=Development` and you can access the HTTPS version of the site at https://localhost:8920. - -By default, the development certificate is not trusted so you will see a security warning when you browse to the site over HTTPS. On most browsers you can easily bypass this warning and continue to the site. However, if you want to get rid of the warning, you can configure your machine to trust the development certificate by following the instructions in the [ASP.NET Core documentation](https://docs.microsoft.com/en-us/aspnet/core/security/enforcing-ssl#trust-the-aspnet-core-https-development-certificate-on-windows-and-macos). From 644ddfad00e2a4b42da89badca70474aef2464ff Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Mon, 6 Apr 2020 19:36:44 -0400 Subject: [PATCH 09/97] Apply code review changes --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index de5ea3f301..d74ec47669 100644 --- a/README.md +++ b/README.md @@ -66,17 +66,17 @@ Most of the translations can be found in the web client but we have several othe ## Jellyfin Server -This repository contains the code for Jellyfin's back-end server. Note that this is only one of many projects/repositories under the Jellyfin GitHub [organization](https://github.com/jellyfin/). If you want to contribute, can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on. +This repository contains the code for Jellyfin's backend server. Note that this is only one of many projects under the Jellyfin GitHub [organization](https://github.com/jellyfin/) on GitHub. If you want to contribute, you can start by checking out our [documentation](https://jellyfin.org/docs/general/contributing/index.html) to see what to work on. ## Server Development -These instructions will help you get set up with a local development environment in order to contribute to this repository. Before you start, please be sure to completely read our [guidelines on development contributions](https://jellyfin.org/docs/general/contributing/development.html). Note that this project is supported on all major operating systems (Windows, Mac and Linux). +These instructions will help you get set up with a local development environment in order to contribute to this repository. Before you start, please be sure to completely read our [guidelines on development contributions](https://jellyfin.org/docs/general/contributing/development.html). Note that this project is supported on all major operating systems except FreeBSD, which is still incompatible. ### Prerequisites Before the project can be built, you must first install the [.NET Core 3.1 SDK](https://dotnet.microsoft.com/download) on your system. -Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but explicit instructions for [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download) are included here. +Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download). ### Cloning the Repository @@ -88,16 +88,20 @@ git clone https://github.com/jellyfin/jellyfin.git ### Installing the Web Client -By default, the server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend API. before you can run the server, you will need to get a copy of the web client files since they are not included in this repository directly. +The server is configured to host the static files required for the [web client](https://github.com/jellyfin/jellyfin-web) in addition to serving the backend by default. Before you can run the server, you will need to get a copy of the web client since they are not included in this repository directly. Note that it is also possible to [host the web client separately](#hosting-the-web-client-separately) from the web server with some additional configuration, in which case you can skip this step. -There are two options to get the files for the web client: +There are three options to get the files for the web client. -1. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) -2. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located here: `C:\Program Files\Jellyfin\Server\jellyfin-web` +1. Download one of the finished builds from the [Azure DevOps pipeline](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11). You can download the build for a specific release by looking at the [branches tab](https://dev.azure.com/jellyfin-project/jellyfin/_build?definitionId=11&_a=summary&repositoryFilter=6&view=branches) of the pipelines page. +2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) +3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` -Once you have a copy of the built web client files, you need to copy them into the build output directory of the web server project. For example: `\Jellyfin.Server\bin\Debug\netcoreapp3.1\jellyfin-web` +Once you have a copy of the built web client files, you need to copy them into the build output directory of the web server project. + +* `/jellyfin-web` +* `/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin-web` ### Running The Server @@ -138,7 +142,7 @@ A second option is to build the project and then run the resulting executable fi ### Running The Tests -This repository also includes several unit test projects that are used to validate functionality as part of a CI process. These are several ways to run these tests: +This repository also includes unit tests that are used to validate functionality as part of a CI pipeline on Azure. There are several ways to run these tests. 1. Run tests from the command line using `dotnet test` 2. Run tests in Visual Studio using the [Test Explorer](https://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer) @@ -150,7 +154,7 @@ The following sections describe some more advanced scenarios for running the ser #### Hosting The Web Client Separately -It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for front-end developers who would prefer to host the client in a separate webpack development server for a tighter development loop (see the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this). +It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this. To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. From 0a9b7c868e798769196e2c93e4359f176f2ceb12 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Tue, 7 Apr 2020 10:42:16 -0400 Subject: [PATCH 10/97] Specify the directory for jellyfin-web correctly --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d74ec47669..6007aa3cb9 100644 --- a/README.md +++ b/README.md @@ -98,10 +98,11 @@ There are three options to get the files for the web client. 2. Build them from source following the instructions on the [jellyfin-web repository](https://github.com/jellyfin/jellyfin-web) 3. Get the pre-built files from an existing installation of the server. For example, with a Windows server installation the client files are located at `C:\Program Files\Jellyfin\Server\jellyfin-web` -Once you have a copy of the built web client files, you need to copy them into the build output directory of the web server project. +Once you have a copy of the built web client files, you need to copy them into a specific directory. -* `/jellyfin-web` -* `/Jellyfin.Server/bin/Debug/netcoreapp3.1/jellyfin-web` +> `/Mediabrowser.WebDashboard/jellyfin-web` + +As part of the build process, this folder will be copied to the build output directory, where it can be accessed by the server. ### Running The Server From 7b2fca96e34339e028350181a51f249b91f71fba Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 9 Apr 2020 15:43:47 +0800 Subject: [PATCH 11/97] use pre-compiled deb to avoid non-free drivers --- Dockerfile | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index caac7500a5..e3a5e41b38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,4 @@ ARG DOTNET_VERSION=3.1 -ARG FFMPEG_VERSION=latest FROM node:alpine as web-builder ARG JELLYFIN_WEB_VERSION=master @@ -17,7 +16,6 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 # see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 "-p:GenerateDocumentationFile=false;DebugSymbols=false;DebugType=none" -FROM jellyfin/ffmpeg:${FFMPEG_VERSION} as ffmpeg FROM debian:buster-slim # https://askubuntu.com/questions/972516/debian-frontend-environment-variable @@ -27,32 +25,26 @@ ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" -COPY --from=ffmpeg /opt/ffmpeg /opt/ffmpeg COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web # Install dependencies: -# libfontconfig1: needed for Skia -# libgomp1: needed for ffmpeg -# libva-drm2: needed for ffmpeg -# mesa-va-drivers: needed for VAAPI +# mesa-va-drivers: needed for AMD VAAPI RUN apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ + && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo apt-key add - \ + && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | sudo tee /etc/apt/sources.list.d/jellyfin.list \ + && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ - libfontconfig1 \ - libgomp1 \ - libva-drm2 \ mesa-va-drivers \ + jellyfin-ffmpeg \ openssl \ - ca-certificates \ - vainfo \ - i965-va-driver \ locales \ - && apt-get clean autoclean -y\ - && apt-get autoremove -y\ + && apt-get remove gnupg wget apt-transport-https -y \ + && apt-get clean autoclean -y \ + && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /cache /config /media \ && chmod 777 /cache /config /media \ - && ln -s /opt/ffmpeg/bin/ffmpeg /usr/local/bin \ - && ln -s /opt/ffmpeg/bin/ffprobe /usr/local/bin \ && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 @@ -65,4 +57,4 @@ VOLUME /cache /config /media ENTRYPOINT ["./jellyfin/jellyfin", \ "--datadir", "/config", \ "--cachedir", "/cache", \ - "--ffmpeg", "/usr/local/bin/ffmpeg"] + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] From 8105494cb5bbab9f8930d7aecc1b7a2a3082c810 Mon Sep 17 00:00:00 2001 From: Nyanmisaka Date: Thu, 9 Apr 2020 17:37:57 +0800 Subject: [PATCH 12/97] minor changes --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e3a5e41b38..6e834d4e0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,8 +31,8 @@ COPY --from=web-builder /dist /jellyfin/jellyfin-web # mesa-va-drivers: needed for AMD VAAPI RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget apt-transport-https \ - && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo apt-key add - \ - && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | sudo tee /etc/apt/sources.list.d/jellyfin.list \ + && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ + && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \ && apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y \ mesa-va-drivers \ From 8e9aeb84b18f43b6fe8dd89ab84f1627bf2e8dbd Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 19:33:36 +0900 Subject: [PATCH 13/97] remove release channel from plugin classes --- .../ApplicationHost.cs | 14 ------------- .../Updates/InstallationManager.cs | 21 +++++++------------ MediaBrowser.Api/PackageService.cs | 10 +-------- MediaBrowser.Common/IApplicationHost.cs | 6 ------ .../Updates/IInstallationManager.cs | 8 ++----- .../Services/IHasRequestFilter.cs | 10 ++++----- MediaBrowser.Model/System/SystemInfo.cs | 2 -- .../Updates/InstallationInfo.cs | 6 ------ MediaBrowser.Model/Updates/ReleaseChannel.cs | 18 ---------------- MediaBrowser.Model/Updates/VersionInfo.cs | 6 ------ 10 files changed, 15 insertions(+), 86 deletions(-) delete mode 100644 MediaBrowser.Model/Updates/ReleaseChannel.cs diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index ad77ab8b48..3cf4d6c6df 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -211,19 +211,6 @@ namespace Emby.Server.Implementations public IFileSystem FileSystemManager { get; set; } - /// - public ReleaseChannel SystemUpdateLevel - { - get - { -#if NIGHTLY - return PackageChannel.Nightly; -#else - return ReleaseChannel.Stable; -#endif - } - } - /// /// Gets or sets the service provider. /// @@ -1416,7 +1403,6 @@ namespace Emby.Server.Implementations SupportsLibraryMonitor = true, EncoderLocation = MediaEncoder.EncoderLocation, SystemArchitecture = RuntimeInformation.OSArchitecture, - SystemUpdateLevel = SystemUpdateLevel, PackageName = StartupOptions.PackageName }; } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 1450c74d2d..a00dec4c3a 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -150,13 +150,11 @@ namespace Emby.Server.Implementations.Updates /// public IEnumerable GetCompatibleVersions( IEnumerable availableVersions, - Version minVersion = null, - ReleaseChannel releaseChannel = ReleaseChannel.Stable) + Version minVersion = null) { var appVer = _applicationHost.ApplicationVersion; availableVersions = availableVersions - .Where(x => x.channel == releaseChannel - && Version.Parse(x.minimumServerVersion) <= appVer); + .Where(x => Version.Parse(x.minimumServerVersion) <= appVer); if (minVersion != null) { @@ -171,8 +169,7 @@ namespace Emby.Server.Implementations.Updates IEnumerable availablePackages, string name = null, Guid guid = default, - Version minVersion = null, - ReleaseChannel releaseChannel = ReleaseChannel.Stable) + Version minVersion = null) { var package = FilterPackages(availablePackages, name, guid).FirstOrDefault(); @@ -184,8 +181,7 @@ namespace Emby.Server.Implementations.Updates return GetCompatibleVersions( package.versions, - minVersion, - releaseChannel); + minVersion); } /// @@ -193,12 +189,10 @@ namespace Emby.Server.Implementations.Updates { var catalog = await GetAvailablePackages(cancellationToken).ConfigureAwait(false); - var systemUpdateLevel = _applicationHost.SystemUpdateLevel; - // Figure out what needs to be installed foreach (var plugin in _applicationHost.Plugins) { - var compatibleVersions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version, systemUpdateLevel); + var compatibleVersions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.versionCode > plugin.Version); if (version != null && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase))) @@ -221,7 +215,6 @@ namespace Emby.Server.Implementations.Updates Id = Guid.NewGuid(), Name = package.name, AssemblyGuid = package.guid, - UpdateClass = package.channel, Version = package.versionString }; @@ -313,13 +306,13 @@ namespace Emby.Server.Implementations.Updates // Do plugin-specific processing if (plugin == null) { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionString ?? string.Empty, package.channel); + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionString ?? string.Empty); PluginInstalled?.Invoke(this, new GenericEventArgs(package)); } else { - _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionString ?? string.Empty, package.channel); + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionString ?? string.Empty); PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package))); } diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index ccc978295c..444354a992 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -71,13 +71,6 @@ namespace MediaBrowser.Api /// The version. [ApiMember(Name = "Version", Description = "Optional version. Defaults to latest version.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] public string Version { get; set; } - - /// - /// Gets or sets the update class. - /// - /// The update class. - [ApiMember(Name = "UpdateClass", Description = "Optional update class (Dev, Beta, Release). Defaults to Release.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public ReleaseChannel UpdateClass { get; set; } } /// @@ -152,8 +145,7 @@ namespace MediaBrowser.Api packages, request.Name, string.IsNullOrEmpty(request.AssemblyGuid) ? Guid.Empty : Guid.Parse(request.AssemblyGuid), - string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version), - request.UpdateClass).FirstOrDefault(); + string.IsNullOrEmpty(request.Version) ? null : Version.Parse(request.Version)).FirstOrDefault(); if (package == null) { diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index c88eac27a1..695e6f8752 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -47,12 +47,6 @@ namespace MediaBrowser.Common /// true if this instance can self restart; otherwise, false. bool CanSelfRestart { get; } - /// - /// Gets the version class of the system. - /// - /// or . - ReleaseChannel SystemUpdateLevel { get; } - /// /// Gets the application version. /// diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index 284e418d9d..4d512220bb 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -65,12 +65,10 @@ namespace MediaBrowser.Common.Updates /// /// The available version of the plugin. /// The minimum required version of the plugin. - /// The classification of updates. /// All compatible versions ordered from newest to oldest. IEnumerable GetCompatibleVersions( IEnumerable availableVersions, - Version minVersion = null, - ReleaseChannel releaseChannel = ReleaseChannel.Stable); + Version minVersion = null); /// /// Returns all compatible versions ordered from newest to oldest. @@ -79,14 +77,12 @@ namespace MediaBrowser.Common.Updates /// The name. /// The guid of the plugin. /// The minimum required version of the plugin. - /// The classification. /// All compatible versions ordered from newest to oldest. IEnumerable GetCompatibleVersions( IEnumerable availablePackages, string name = null, Guid guid = default, - Version minVersion = null, - ReleaseChannel releaseChannel = ReleaseChannel.Stable); + Version minVersion = null); /// /// Returns the available plugin updates. diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs index c81e49e4eb..418d5c501e 100644 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ b/MediaBrowser.Model/Services/IHasRequestFilter.cs @@ -9,17 +9,17 @@ namespace MediaBrowser.Model.Services { /// /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters + /// <0 Executed before global request filters. + /// >0 Executed after global request filters. /// int Priority { get; } /// /// The request filter is executed before the service. /// - /// The http request wrapper - /// The http response wrapper - /// The request DTO + /// The http request wrapper. + /// The http response wrapper. + /// The request DTO. void RequestFilter(IRequest req, HttpResponse res, object requestDto); } } diff --git a/MediaBrowser.Model/System/SystemInfo.cs b/MediaBrowser.Model/System/SystemInfo.cs index da39ee208a..bda43e1afa 100644 --- a/MediaBrowser.Model/System/SystemInfo.cs +++ b/MediaBrowser.Model/System/SystemInfo.cs @@ -27,8 +27,6 @@ namespace MediaBrowser.Model.System /// public class SystemInfo : PublicSystemInfo { - public ReleaseChannel SystemUpdateLevel { get; set; } - /// /// Gets or sets the display name of the operating system. /// diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index 870bf8c0be..95357262ac 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -30,11 +30,5 @@ namespace MediaBrowser.Model.Updates /// /// The version. public string Version { get; set; } - - /// - /// Gets or sets the update class. - /// - /// The update class. - public ReleaseChannel UpdateClass { get; set; } } } diff --git a/MediaBrowser.Model/Updates/ReleaseChannel.cs b/MediaBrowser.Model/Updates/ReleaseChannel.cs deleted file mode 100644 index ed4a774a72..0000000000 --- a/MediaBrowser.Model/Updates/ReleaseChannel.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// - /// Enum PackageVersionClass. - /// - public enum ReleaseChannel - { - /// - /// The stable. - /// - Stable = 0, - - /// - /// The nightly. - /// - Nightly = 1 - } -} diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index ad893db2e2..177b8dbc33 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -34,12 +34,6 @@ namespace MediaBrowser.Model.Updates /// The version. public Version versionCode { get; set; } - /// - /// Gets or sets the release channel. - /// - /// The release channel for a given package version. - public ReleaseChannel channel { get; set; } - /// /// Gets or sets the description. /// From 78abbcc25117a20511f70f65c89f2f1063d0e547 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 19:52:40 +0900 Subject: [PATCH 14/97] standardize plugin version and guid properties --- .../Activity/ActivityLogEntryPoint.cs | 4 +-- .../Updates/InstallationManager.cs | 21 +++++++------- .../Updates/CheckForUpdateResult.cs | 29 ------------------- .../Updates/InstallationInfo.cs | 12 ++------ MediaBrowser.Model/Updates/PackageInfo.cs | 12 -------- MediaBrowser.Model/Updates/VersionInfo.cs | 8 +---- 6 files changed, 16 insertions(+), 70 deletions(-) delete mode 100644 MediaBrowser.Model/Updates/CheckForUpdateResult.cs diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 0f0b8b97b1..f60fdea846 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -433,7 +433,7 @@ namespace Emby.Server.Implementations.Activity ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.Item2.versionString), + e.Argument.Item2.version), Overview = e.Argument.Item2.description }); } @@ -462,7 +462,7 @@ namespace Emby.Server.Implementations.Activity ShortOverview = string.Format( CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), - e.Argument.versionString) + e.Argument.version) }); } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index a00dec4c3a..ffb6b5cbc3 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -158,10 +158,10 @@ namespace Emby.Server.Implementations.Updates if (minVersion != null) { - availableVersions = availableVersions.Where(x => x.versionCode >= minVersion); + availableVersions = availableVersions.Where(x => x.version >= minVersion); } - return availableVersions.OrderByDescending(x => x.versionCode); + return availableVersions.OrderByDescending(x => x.version); } /// @@ -193,9 +193,9 @@ namespace Emby.Server.Implementations.Updates foreach (var plugin in _applicationHost.Plugins) { var compatibleVersions = GetCompatibleVersions(catalog, plugin.Name, plugin.Id, plugin.Version); - var version = compatibleVersions.FirstOrDefault(y => y.versionCode > plugin.Version); + var version = compatibleVersions.FirstOrDefault(y => y.version > plugin.Version); if (version != null - && !CompletedInstallations.Any(x => string.Equals(x.AssemblyGuid, version.guid, StringComparison.OrdinalIgnoreCase))) + && !CompletedInstallations.Any(x => string.Equals(x.Guid, version.guid, StringComparison.OrdinalIgnoreCase))) { yield return version; } @@ -212,10 +212,9 @@ namespace Emby.Server.Implementations.Updates var installationInfo = new InstallationInfo { - Id = Guid.NewGuid(), + Guid = package.guid, Name = package.name, - AssemblyGuid = package.guid, - Version = package.versionString + Version = package.version.ToString() }; var innerCancellationTokenSource = new CancellationTokenSource(); @@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.Updates _currentInstallations.Remove(tuple); } - _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionString); + _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.version); PackageInstallationCancelled?.Invoke(this, installationEventArgs); @@ -306,13 +305,13 @@ namespace Emby.Server.Implementations.Updates // Do plugin-specific processing if (plugin == null) { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionString ?? string.Empty); + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.version); PluginInstalled?.Invoke(this, new GenericEventArgs(package)); } else { - _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionString ?? string.Empty); + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.version); PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, VersionInfo)>((plugin, package))); } @@ -430,7 +429,7 @@ namespace Emby.Server.Implementations.Updates { lock (_currentInstallationsLock) { - var install = _currentInstallations.Find(x => x.info.Id == id); + var install = _currentInstallations.Find(x => x.info.Guid == id.ToString()); if (install == default((InstallationInfo, CancellationTokenSource))) { return false; diff --git a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs b/MediaBrowser.Model/Updates/CheckForUpdateResult.cs deleted file mode 100644 index 883fc636b4..0000000000 --- a/MediaBrowser.Model/Updates/CheckForUpdateResult.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace MediaBrowser.Model.Updates -{ - /// - /// Class CheckForUpdateResult. - /// - public class CheckForUpdateResult - { - /// - /// Gets or sets a value indicating whether this instance is update available. - /// - /// true if this instance is update available; otherwise, false. - public bool IsUpdateAvailable { get; set; } - - /// - /// Gets or sets the available version. - /// - /// The available version. - public string AvailableVersion - { - get => Package != null ? Package.versionString : "0.0.0.1"; - set { } // need this for the serializer - } - - /// - /// Get or sets package information for an available update - /// - public VersionInfo Package { get; set; } - } -} diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index 95357262ac..e0d450d065 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -8,10 +8,10 @@ namespace MediaBrowser.Model.Updates public class InstallationInfo { /// - /// Gets or sets the id. + /// Gets or sets the guid. /// - /// The id. - public Guid Id { get; set; } + /// The guid. + public string Guid { get; set; } /// /// Gets or sets the name. @@ -19,12 +19,6 @@ namespace MediaBrowser.Model.Updates /// The name. public string Name { get; set; } - /// - /// Gets or sets the assembly guid. - /// - /// The guid of the assembly. - public string AssemblyGuid { get; set; } - /// /// Gets or sets the version. /// diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index d06ffe1e6c..b19c311e4b 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -26,18 +26,6 @@ namespace MediaBrowser.Model.Updates /// The overview. public string overview { get; set; } - /// - /// Gets or sets the thumb image. - /// - /// The thumb image. - public string thumbImage { get; set; } - - /// - /// Gets or sets the preview image. - /// - /// The preview image. - public string previewImage { get; set; } - /// /// Gets or sets the target filename for the downloaded binary. /// diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 177b8dbc33..6756e7454a 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -22,17 +22,11 @@ namespace MediaBrowser.Model.Updates /// The guid. public string guid { get; set; } - /// - /// Gets or sets the version string. - /// - /// The version string. - public string versionString { get; set; } - /// /// Gets or sets the version. /// /// The version. - public Version versionCode { get; set; } + public Version version { get; set; } /// /// Gets or sets the description. From abb7ed9c35cc7727e81357c3b55555ee4c714d10 Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 19:54:33 +0900 Subject: [PATCH 15/97] rename target abi property --- Emby.Server.Implementations/Updates/InstallationManager.cs | 2 +- MediaBrowser.Model/Updates/VersionInfo.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index ffb6b5cbc3..6a34ca74b0 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -154,7 +154,7 @@ namespace Emby.Server.Implementations.Updates { var appVer = _applicationHost.ApplicationVersion; availableVersions = availableVersions - .Where(x => Version.Parse(x.minimumServerVersion) <= appVer); + .Where(x => Version.Parse(x.targetAbi) <= appVer); if (minVersion != null) { diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index 6756e7454a..ac47e97daa 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -35,10 +35,10 @@ namespace MediaBrowser.Model.Updates public string description { get; set; } /// - /// Gets or sets the minimum required version for the server. + /// Gets or sets the ABI that this version was built against. /// - /// The minimum required version. - public string minimumServerVersion { get; set; } + /// The target ABI version. + public string targetAbi { get; set; } /// /// Gets or sets the source URL. From 2da5df6c25b96e2a2d6790df7df8066a666dbe0e Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 19:56:42 +0900 Subject: [PATCH 16/97] add new property for version changelogs --- .../Activity/ActivityLogEntryPoint.cs | 2 +- MediaBrowser.Model/Updates/VersionInfo.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index f60fdea846..c91506b85b 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -434,7 +434,7 @@ namespace Emby.Server.Implementations.Activity CultureInfo.InvariantCulture, _localization.GetLocalizedString("VersionNumber"), e.Argument.Item2.version), - Overview = e.Argument.Item2.description + Overview = e.Argument.Item2.changelog }); } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index ac47e97daa..e67e60d745 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -29,10 +29,10 @@ namespace MediaBrowser.Model.Updates public Version version { get; set; } /// - /// Gets or sets the description. + /// Gets or sets the changelog for this version. /// - /// The description. - public string description { get; set; } + /// The changelog. + public string changelog { get; set; } /// /// Gets or sets the ABI that this version was built against. From cbe1fe2c8f82af2233201203367ba8a532dbec8b Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 20:00:32 +0900 Subject: [PATCH 17/97] update description and overview for plugins --- MediaBrowser.Model/Updates/PackageInfo.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/MediaBrowser.Model/Updates/PackageInfo.cs b/MediaBrowser.Model/Updates/PackageInfo.cs index b19c311e4b..f5aa8b6fa2 100644 --- a/MediaBrowser.Model/Updates/PackageInfo.cs +++ b/MediaBrowser.Model/Updates/PackageInfo.cs @@ -15,23 +15,17 @@ namespace MediaBrowser.Model.Updates public string name { get; set; } /// - /// Gets or sets the short description. + /// Gets or sets a long description of the plugin containing features or helpful explanations. /// - /// The short description. - public string shortDescription { get; set; } + /// The description. + public string description { get; set; } /// - /// Gets or sets the overview. + /// Gets or sets a short overview of what the plugin does. /// /// The overview. public string overview { get; set; } - /// - /// Gets or sets the target filename for the downloaded binary. - /// - /// The target filename. - public string filename { get; set; } - /// /// Gets or sets the owner. /// From ff065df9863f30a4eec52a95d260cdadcced7b1e Mon Sep 17 00:00:00 2001 From: dkanada Date: Sat, 11 Apr 2020 20:11:41 +0900 Subject: [PATCH 18/97] update plugin manifest url --- Emby.Server.Implementations/ConfigurationOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 20bdd18e70..db7c35a7c8 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -18,7 +18,7 @@ namespace Emby.Server.Implementations { { HostWebClientKey, bool.TrueString }, { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, - { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest.json" }, + { InstallationManager.PluginManifestUrlKey, "https://repo.jellyfin.org/releases/plugin/manifest-stable.json" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString } From b52199e9e20dcd10c1921e43c53892ec5ec1c5eb Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Mon, 13 Apr 2020 12:44:15 +1000 Subject: [PATCH 19/97] Handle null outputFileExtension in GetOutputFilePath --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index eb44cb4266..b92ea6d1f4 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -131,6 +131,8 @@ namespace MediaBrowser.Api.Playback /// private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension) { + if (outputFileExtension == null) outputFileExtension = ""; + var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); From 1180b9746fe7c4a6562baff77910819a6706510b Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 00:01:31 -0600 Subject: [PATCH 20/97] Migrates the notifications service to use ASP.NET MVC framework --- .../Api/NotificationsService.cs | 189 ------------------ .../Controllers/NotificationsController.cs | 138 +++++++++++++ .../NotificationDtos/NotificationDto.cs | 51 +++++ .../NotificationsSummaryDto.cs | 20 ++ .../ApiServiceCollectionExtensions.cs | 1 + 5 files changed, 210 insertions(+), 189 deletions(-) delete mode 100644 Emby.Notifications/Api/NotificationsService.cs create mode 100644 Jellyfin.Api/Controllers/NotificationsController.cs create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs diff --git a/Emby.Notifications/Api/NotificationsService.cs b/Emby.Notifications/Api/NotificationsService.cs deleted file mode 100644 index 788750796d..0000000000 --- a/Emby.Notifications/Api/NotificationsService.cs +++ /dev/null @@ -1,189 +0,0 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1402 -#pragma warning disable SA1649 - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Notifications; -using MediaBrowser.Model.Dto; -using MediaBrowser.Model.Notifications; -using MediaBrowser.Model.Services; - -namespace Emby.Notifications.Api -{ - [Route("/Notifications/{UserId}", "GET", Summary = "Gets notifications")] - public class GetNotifications : IReturn - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "IsRead", Description = "An optional filter by IsRead", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] - public bool? IsRead { get; set; } - - [ApiMember(Name = "StartIndex", Description = "Optional. The record index to start at. All items with a lower index will be dropped from the results.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? StartIndex { get; set; } - - [ApiMember(Name = "Limit", Description = "Optional. The maximum number of records to return", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] - public int? Limit { get; set; } - } - - public class Notification - { - public string Id { get; set; } = string.Empty; - - public string UserId { get; set; } = string.Empty; - - public DateTime Date { get; set; } - - public bool IsRead { get; set; } - - public string Name { get; set; } = string.Empty; - - public string Description { get; set; } = string.Empty; - - public string Url { get; set; } = string.Empty; - - public NotificationLevel Level { get; set; } - } - - public class NotificationResult - { - public IReadOnlyList Notifications { get; set; } = Array.Empty(); - - public int TotalRecordCount { get; set; } - } - - public class NotificationsSummary - { - public int UnreadCount { get; set; } - - public NotificationLevel MaxUnreadNotificationLevel { get; set; } - } - - [Route("/Notifications/{UserId}/Summary", "GET", Summary = "Gets a notification summary for a user")] - public class GetNotificationsSummary : IReturn - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string UserId { get; set; } = string.Empty; - } - - [Route("/Notifications/Types", "GET", Summary = "Gets notification types")] - public class GetNotificationTypes : IReturn> - { - } - - [Route("/Notifications/Services", "GET", Summary = "Gets notification types")] - public class GetNotificationServices : IReturn> - { - } - - [Route("/Notifications/Admin", "POST", Summary = "Sends a notification to all admin users")] - public class AddAdminNotification : IReturnVoid - { - [ApiMember(Name = "Name", Description = "The notification's name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } = string.Empty; - - [ApiMember(Name = "Description", Description = "The notification's description", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Description { get; set; } = string.Empty; - - [ApiMember(Name = "ImageUrl", Description = "The notification's image url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string? ImageUrl { get; set; } - - [ApiMember(Name = "Url", Description = "The notification's info url", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public string? Url { get; set; } - - [ApiMember(Name = "Level", Description = "The notification level", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "POST")] - public NotificationLevel Level { get; set; } - } - - [Route("/Notifications/{UserId}/Read", "POST", Summary = "Marks notifications as read")] - public class MarkRead : IReturnVoid - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } = string.Empty; - } - - [Route("/Notifications/{UserId}/Unread", "POST", Summary = "Marks notifications as unread")] - public class MarkUnread : IReturnVoid - { - [ApiMember(Name = "UserId", Description = "User Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string UserId { get; set; } = string.Empty; - - [ApiMember(Name = "Ids", Description = "A list of notification ids, comma delimited", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST", AllowMultiple = true)] - public string Ids { get; set; } = string.Empty; - } - - [Authenticated] - public class NotificationsService : IService - { - private readonly INotificationManager _notificationManager; - private readonly IUserManager _userManager; - - public NotificationsService(INotificationManager notificationManager, IUserManager userManager) - { - _notificationManager = notificationManager; - _userManager = userManager; - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationTypes request) - { - return _notificationManager.GetNotificationTypes(); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationServices request) - { - return _notificationManager.GetNotificationServices().ToList(); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotificationsSummary request) - { - return new NotificationsSummary - { - }; - } - - public Task Post(AddAdminNotification request) - { - // This endpoint really just exists as post of a real with sickbeard - var notification = new NotificationRequest - { - Date = DateTime.UtcNow, - Description = request.Description, - Level = request.Level, - Name = request.Name, - Url = request.Url, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray() - }; - - return _notificationManager.SendNotification(notification, CancellationToken.None); - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public void Post(MarkRead request) - { - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public void Post(MarkUnread request) - { - } - - [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "request", Justification = "Required for ServiceStack")] - public object Get(GetNotifications request) - { - return new NotificationResult(); - } - } -} diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs new file mode 100644 index 0000000000..6602fca9c7 --- /dev/null +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -0,0 +1,138 @@ +#nullable enable +#pragma warning disable CA1801 +#pragma warning disable SA1313 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Jellyfin.Api.Models.NotificationDtos; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Notifications; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// + /// The notification controller. + /// + public class NotificationsController : BaseJellyfinApiController + { + private readonly INotificationManager _notificationManager; + private readonly IUserManager _userManager; + + /// + /// Initializes a new instance of the class. + /// + /// The notification manager. + /// The user manager. + public NotificationsController(INotificationManager notificationManager, IUserManager userManager) + { + _notificationManager = notificationManager; + _userManager = userManager; + } + + /// + /// Endpoint for getting a user's notifications. + /// + /// The UserID. + /// An optional filter by IsRead. + /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// An optional limit on the number of notifications returned. + /// A read-only list of all of the user's notifications. + [HttpGet("{UserID}")] + public IReadOnlyList GetNotifications( + [FromRoute] string UserID, + [FromQuery] bool? IsRead, + [FromQuery] int? StartIndex, + [FromQuery] int? Limit) + { + return new List(); + } + + /// + /// Endpoint for getting a user's notification summary. + /// + /// The UserID. + /// Notifications summary for the user. + [HttpGet("{UserId}/Summary")] + public NotificationsSummaryDto GetNotificationsSummary( + [FromRoute] string UserID) + { + return new NotificationsSummaryDto(); + } + + /// + /// Endpoint for getting notification types. + /// + /// All notification types. + [HttpGet("Types")] + public IEnumerable GetNotificationTypes() + { + return _notificationManager.GetNotificationTypes(); + } + + /// + /// Endpoint for getting notification services. + /// + /// All notification services. + [HttpGet("Services")] + public IEnumerable GetNotificationServices() + { + return _notificationManager.GetNotificationServices(); + } + + /// + /// Endpoint to send a notification to all admins. + /// + /// The name of the notification. + /// The description of the notification. + /// The URL of the notification. + /// The level of the notification. + [HttpPost("Admin")] + public void CreateAdminNotification( + [FromForm] string Name, + [FromForm] string Description, + [FromForm] string? URL, + [FromForm] NotificationLevel Level) + { + var notification = new NotificationRequest + { + Name = Name, + Description = Description, + Url = URL, + Level = Level, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; + + _notificationManager.SendNotification(notification, CancellationToken.None); + } + + /// + /// Endpoint to set notifications as read. + /// + /// The UserID. + /// The IDs of notifications which should be set as read. + [HttpPost("{UserID}/Read")] + public void SetRead( + [FromRoute] string UserID, + [FromForm] List IDs) + { + } + + /// + /// Endpoint to set notifications as unread. + /// + /// The UserID. + /// The IDs of notifications which should be set as unread. + [HttpPost("{UserID}/Unread")] + public void SetUnread( + [FromRoute] string UserID, + [FromForm] List IDs) + { + } + } +} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs new file mode 100644 index 0000000000..7ecd2a49db --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -0,0 +1,51 @@ +using System; +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// The notification DTO. + /// + public class NotificationDto + { + /// + /// Gets or sets the notification ID. Defaults to an empty string. + /// + public string Id { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's user ID. Defaults to an empty string. + /// + public string UserId { get; set; } = string.Empty; + + /// + /// Gets or sets the notification date. + /// + public DateTime Date { get; set; } + + /// + /// Gets or sets a value indicating whether the notification has been read. + /// + public bool IsRead { get; set; } + + /// + /// Gets or sets the notification's name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the notification's URL. + /// + public string Url { get; set; } = string.Empty; + + /// + /// Gets or sets the notification level. + /// + public NotificationLevel Level { get; set; } + } +} diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs new file mode 100644 index 0000000000..c18ab545d3 --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -0,0 +1,20 @@ +using MediaBrowser.Model.Notifications; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// The notification summary DTO. + /// + public class NotificationsSummaryDto + { + /// + /// Gets or sets the number of unread notifications. + /// + public int UnreadCount { get; set; } + + /// + /// Gets or sets the maximum unread notification level. + /// + public NotificationLevel MaxUnreadNotificationLevel { get; set; } + } +} diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index dd4f9cd238..b3164e068f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,6 +71,7 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) + .AddApplicationPart(typeof(NotificationsController).Assembly) .AddControllersAsServices(); } From ad1c880751dda93f1226e3846bb6a344ac58d0b6 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 00:34:50 -0600 Subject: [PATCH 21/97] Lowercase parameters --- .../Controllers/NotificationsController.cs | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 6602fca9c7..31747584e1 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -1,6 +1,5 @@ #nullable enable #pragma warning disable CA1801 -#pragma warning disable SA1313 using System; using System.Collections.Generic; @@ -37,17 +36,17 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notifications. /// - /// The UserID. - /// An optional filter by IsRead. - /// The optional index to start at. All notifications with a lower index will be dropped from the results. - /// An optional limit on the number of notifications returned. + /// The UserID. + /// An optional filter by IsRead. + /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] public IReadOnlyList GetNotifications( - [FromRoute] string UserID, - [FromQuery] bool? IsRead, - [FromQuery] int? StartIndex, - [FromQuery] int? Limit) + [FromRoute] string userID, + [FromQuery] bool? isRead, + [FromQuery] int? startIndex, + [FromQuery] int? limit) { return new List(); } @@ -55,11 +54,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notification summary. /// - /// The UserID. + /// The userID. /// Notifications summary for the user. - [HttpGet("{UserId}/Summary")] + [HttpGet("{UserID}/Summary")] public NotificationsSummaryDto GetNotificationsSummary( - [FromRoute] string UserID) + [FromRoute] string userID) { return new NotificationsSummaryDto(); } @@ -87,23 +86,23 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to send a notification to all admins. /// - /// The name of the notification. - /// The description of the notification. - /// The URL of the notification. - /// The level of the notification. + /// The name of the notification. + /// The description of the notification. + /// The URL of the notification. + /// The level of the notification. [HttpPost("Admin")] public void CreateAdminNotification( - [FromForm] string Name, - [FromForm] string Description, - [FromForm] string? URL, - [FromForm] NotificationLevel Level) + [FromForm] string name, + [FromForm] string description, + [FromForm] string? url, + [FromForm] NotificationLevel level) { var notification = new NotificationRequest { - Name = Name, - Description = Description, - Url = URL, - Level = Level, + Name = name, + Description = description, + Url = url, + Level = level, UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), Date = DateTime.UtcNow, }; @@ -114,24 +113,24 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as read. /// - /// The UserID. - /// The IDs of notifications which should be set as read. + /// The userID. + /// The IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( - [FromRoute] string UserID, - [FromForm] List IDs) + [FromRoute] string userID, + [FromForm] List ids) { } /// /// Endpoint to set notifications as unread. /// - /// The UserID. - /// The IDs of notifications which should be set as unread. + /// The userID. + /// The IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( - [FromRoute] string UserID, - [FromForm] List IDs) + [FromRoute] string userID, + [FromForm] List ids) { } } From 558b50a094adc82728a52b13862e19bc04783679 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 15 Apr 2020 09:29:29 -0600 Subject: [PATCH 22/97] Remove unnecessary assembly, update casing, enable nullable reference types on notification DTOs. --- .../Controllers/NotificationsController.cs | 20 +++++++++---------- .../NotificationDtos/NotificationDto.cs | 16 ++++++++------- .../NotificationsSummaryDto.cs | 4 +++- .../ApiServiceCollectionExtensions.cs | 1 - 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 31747584e1..c8a5be89b3 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -36,14 +36,14 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notifications. /// - /// The UserID. + /// The user's ID. /// An optional filter by IsRead. /// The optional index to start at. All notifications with a lower index will be dropped from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] public IReadOnlyList GetNotifications( - [FromRoute] string userID, + [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) @@ -54,11 +54,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting a user's notification summary. /// - /// The userID. + /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] public NotificationsSummaryDto GetNotificationsSummary( - [FromRoute] string userID) + [FromRoute] string userId) { return new NotificationsSummaryDto(); } @@ -95,14 +95,14 @@ namespace Jellyfin.Api.Controllers [FromForm] string name, [FromForm] string description, [FromForm] string? url, - [FromForm] NotificationLevel level) + [FromForm] NotificationLevel? level) { var notification = new NotificationRequest { Name = name, Description = description, Url = url, - Level = level, + Level = level ?? NotificationLevel.Normal, UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), Date = DateTime.UtcNow, }; @@ -113,11 +113,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as read. /// - /// The userID. + /// The userID. /// The IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( - [FromRoute] string userID, + [FromRoute] string userId, [FromForm] List ids) { } @@ -125,11 +125,11 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint to set notifications as unread. /// - /// The userID. + /// The userID. /// The IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( - [FromRoute] string userID, + [FromRoute] string userId, [FromForm] List ids) { } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index 7ecd2a49db..c849ecd75d 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using MediaBrowser.Model.Notifications; @@ -24,28 +26,28 @@ namespace Jellyfin.Api.Models.NotificationDtos public DateTime Date { get; set; } /// - /// Gets or sets a value indicating whether the notification has been read. + /// Gets or sets a value indicating whether the notification has been read. Defaults to false. /// - public bool IsRead { get; set; } + public bool IsRead { get; set; } = false; /// - /// Gets or sets the notification's name. + /// Gets or sets the notification's name. Defaults to an empty string. /// public string Name { get; set; } = string.Empty; /// - /// Gets or sets the notification's description. + /// Gets or sets the notification's description. Defaults to an empty string. /// public string Description { get; set; } = string.Empty; /// - /// Gets or sets the notification's URL. + /// Gets or sets the notification's URL. Defaults to null. /// - public string Url { get; set; } = string.Empty; + public string? Url { get; set; } /// /// Gets or sets the notification level. /// - public NotificationLevel Level { get; set; } + public NotificationLevel? Level { get; set; } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs index c18ab545d3..b3746ee2da 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationsSummaryDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using MediaBrowser.Model.Notifications; namespace Jellyfin.Api.Models.NotificationDtos @@ -15,6 +17,6 @@ namespace Jellyfin.Api.Models.NotificationDtos /// /// Gets or sets the maximum unread notification level. /// - public NotificationLevel MaxUnreadNotificationLevel { get; set; } + public NotificationLevel? MaxUnreadNotificationLevel { get; set; } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b3164e068f..dd4f9cd238 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -71,7 +71,6 @@ namespace Jellyfin.Server.Extensions // Clear app parts to avoid other assemblies being picked up .ConfigureApplicationPartManager(a => a.ApplicationParts.Clear()) .AddApplicationPart(typeof(StartupController).Assembly) - .AddApplicationPart(typeof(NotificationsController).Assembly) .AddControllersAsServices(); } From e7fde6aacaf46183d04777c511c72d49300c09cd Mon Sep 17 00:00:00 2001 From: Matt Lyons Date: Fri, 17 Apr 2020 16:13:56 +1000 Subject: [PATCH 23/97] Handle null outputFileExtension with null-conditional operator --- MediaBrowser.Api/Playback/BaseStreamingService.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index b92ea6d1f4..7705393576 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -131,12 +131,10 @@ namespace MediaBrowser.Api.Playback /// private string GetOutputFilePath(StreamState state, EncodingOptions encodingOptions, string outputFileExtension) { - if (outputFileExtension == null) outputFileExtension = ""; - var data = $"{state.MediaPath}-{state.UserAgent}-{state.Request.DeviceId}-{state.Request.PlaySessionId}"; var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); - var ext = outputFileExtension.ToLowerInvariant(); + var ext = outputFileExtension?.ToLowerInvariant(); var folder = ServerConfigurationManager.GetTranscodePath(); return EnableOutputInSubFolder From eba781eac565b0639cba04d6ba98e2210d6cd35a Mon Sep 17 00:00:00 2001 From: a1 Date: Thu, 16 Apr 2020 23:36:43 -0700 Subject: [PATCH 24/97] Fix DLNA clients displaying wrong album art. --- Emby.Dlna/Didl/DidlBuilder.cs | 53 ++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 88cc00e30a..59951f6d9b 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -1018,19 +1018,58 @@ namespace Emby.Dlna.Didl } } - item = item.GetParents().FirstOrDefault(i => i.HasImage(ImageType.Primary)); - - if (item != null) + // For audio tracks without art use album art if available. + if (item is Audio audioItem) { - if (item.HasImage(ImageType.Primary)) - { - return GetImageInfo(item, ImageType.Primary); - } + var album = audioItem.AlbumEntity; + return album != null && album.HasImage(ImageType.Primary) + ? GetImageInfo(album, ImageType.Primary) + : null; + } + + // Don't look beyond album/playlist level. Metadata service may assign an image from a different album/show to the parent folder. + if (item is MusicAlbum || item is Playlist) + { + return null; + } + + // For other item types check parents, but be aware that image retrieved from a parent may be not suitable for this media item. + var parentWithImage = GetFirstParentWithImageBelowUserRoot(item); + if (parentWithImage != null) + { + return GetImageInfo(parentWithImage, ImageType.Primary); } return null; } + private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item) + { + if (item == null) + { + return null; + } + + if (item.HasImage(ImageType.Primary)) + { + return item; + } + + var parent = item.GetParent(); + if (parent is UserRootFolder) + { + return null; + } + + // terminate in case we went past user root folder (unlikely?) + if (parent is Folder folder && folder.IsRoot) + { + return null; + } + + return GetFirstParentWithImageBelowUserRoot(parent); + } + private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type) { var imageInfo = item.GetImageInfo(type, 0); From d7a71cee3ced19b43bdc1ee1523f236de15de26a Mon Sep 17 00:00:00 2001 From: randrey Date: Sat, 18 Apr 2020 17:26:22 -0700 Subject: [PATCH 25/97] Fix imdbid regex --- Emby.Server.Implementations/Library/PathExtensions.cs | 2 +- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 4fdf73b773..5e863b82d4 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt\\d{7}", RegexOptions.IgnoreCase); + var m = Regex.Match(str, "tt\\d{7,}", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 36b9a9c1f8..82802041e9 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -208,8 +208,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 digits - var m = Regex.Match(xml, @"tt([0-9]{7})", RegexOptions.IgnoreCase); + // Look for a match for the Regex pattern "tt" followed by 7 or more digits + var m = Regex.Match(xml, @"tt(\d{7,})", RegexOptions.IgnoreCase); if (m.Success) { item.SetProviderId(MetadataProviders.Imdb, m.Value); From 92f273cb0cd94923be25b6b69147eeb9b86749b0 Mon Sep 17 00:00:00 2001 From: randrey Date: Sat, 18 Apr 2020 18:18:48 -0700 Subject: [PATCH 26/97] Limit imdbid to 8 digits. --- Emby.Server.Implementations/Library/PathExtensions.cs | 2 +- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 5e863b82d4..67c0e4eae7 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt\\d{7,}", RegexOptions.IgnoreCase); + var m = Regex.Match(str, "tt\\d{7,8}", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 82802041e9..0ac94834ea 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -208,8 +208,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 or more digits - var m = Regex.Match(xml, @"tt(\d{7,})", RegexOptions.IgnoreCase); + // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits + var m = Regex.Match(xml, @"tt(\d{7,8})", RegexOptions.IgnoreCase); if (m.Success) { item.SetProviderId(MetadataProviders.Imdb, m.Value); From a73e1f18b670ac7c9f0f9c3049e7fbbeddb61942 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sun, 19 Apr 2020 11:16:09 +0200 Subject: [PATCH 27/97] Minor improvements --- MediaBrowser.Api/UserLibrary/ItemsService.cs | 4 ++-- MediaBrowser.Controller/Entities/BaseItem.cs | 5 ++--- MediaBrowser.Controller/Entities/Folder.cs | 2 +- MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Api/UserLibrary/ItemsService.cs b/MediaBrowser.Api/UserLibrary/ItemsService.cs index ac59c30301..c4d44042b1 100644 --- a/MediaBrowser.Api/UserLibrary/ItemsService.cs +++ b/MediaBrowser.Api/UserLibrary/ItemsService.cs @@ -242,11 +242,11 @@ namespace MediaBrowser.Api.UserLibrary return folder.GetItems(GetItemsQuery(request, dtoOptions, user)); } - var itemsArray = folder.GetChildren(user, true).ToArray(); + var itemsArray = folder.GetChildren(user, true); return new QueryResult { Items = itemsArray, - TotalRecordCount = itemsArray.Length, + TotalRecordCount = itemsArray.Count, StartIndex = 0 }; } diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 56a361e0ec..b9e1fe79d5 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2741,7 +2741,7 @@ namespace MediaBrowser.Controller.Entities { var list = GetEtagValues(user); - return string.Join("|", list.ToArray()).GetMD5().ToString("N", CultureInfo.InvariantCulture); + return string.Join("|", list).GetMD5().ToString("N", CultureInfo.InvariantCulture); } protected virtual List GetEtagValues(User user) @@ -2784,8 +2784,7 @@ namespace MediaBrowser.Controller.Entities return true; } - var view = this as IHasCollectionType; - if (view != null) + if (this is IHasCollectionType view) { if (string.Equals(view.CollectionType, CollectionType.LiveTv, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index bb48605e55..a468e0c35a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -864,7 +864,7 @@ namespace MediaBrowser.Controller.Entities return SortItemsByRequest(query, result); } - return result.ToArray(); + return result; } return GetItemsInternal(query).Items; diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs index c2bfac9d63..3593304491 100644 --- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs +++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs @@ -478,7 +478,7 @@ namespace MediaBrowser.MediaEncoding.Encoder } catch (Exception ex) { - _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {arguments}", inputArgument); + _logger.LogError(ex, "I-frame image extraction failed, will attempt standard way. Input: {Arguments}", inputArgument); } } @@ -969,7 +969,7 @@ namespace MediaBrowser.MediaEncoding.Encoder public int? ExitCode { get; private set; } - void OnProcessExited(object sender, EventArgs e) + private void OnProcessExited(object sender, EventArgs e) { var process = (Process)sender; From 8a7e4cd639be24eb58385dc7b36b466c3d6aed92 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 10:51:51 -0600 Subject: [PATCH 28/97] add redoc --- Jellyfin.Api/Jellyfin.Api.csproj | 3 ++- .../ApiApplicationBuilderExtensions.cs | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 8f23ef9d03..cbb1d3007f 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -10,7 +10,8 @@ - + + diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index db06eb4552..2ab9b0ba5e 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -14,14 +14,18 @@ namespace Jellyfin.Server.Extensions /// The updated application builder. public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder) { - applicationBuilder.UseSwagger(); - // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - return applicationBuilder.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); - }); + const string specEndpoint = "/swagger/v1/swagger.json"; + return applicationBuilder.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); + }) + .UseReDoc(c => + { + c.SpecUrl(specEndpoint); + }); } } } From e72a543570b59df61f48cb9a4049ab3dc9675250 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:24:03 -0600 Subject: [PATCH 29/97] Add Redoc, move docs to api-docs/ --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 2ab9b0ba5e..766243f201 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -21,10 +21,12 @@ namespace Jellyfin.Server.Extensions .UseSwaggerUI(c => { c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); + c.RoutePrefix = "api-docs/swagger"; }) .UseReDoc(c => { c.SpecUrl(specEndpoint); + c.RoutePrefix = "api-docs/redoc"; }); } } From 5da88fac4d0681126bdee635d59237d8d7fcebeb Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:24:32 -0600 Subject: [PATCH 30/97] Enable string enum converter --- .../ApiServiceCollectionExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..a4f078b5b3 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,3 +1,8 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; @@ -75,6 +80,9 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; + + // Accept string enums + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }) .AddControllersAsServices(); } @@ -89,6 +97,17 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + + // Add all xml doc files to swagger generator. + var xmlFiles = Directory.GetFiles( + AppContext.BaseDirectory, + "*.xml", + SearchOption.TopDirectoryOnly); + + foreach (var xmlFile in xmlFiles) + { + c.IncludeXmlComments(xmlFile); + } }); } } From 72745f47225a5b1071660acc4dcde618d938eaa0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 11:28:56 -0600 Subject: [PATCH 31/97] fix formatting --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 766243f201..43c49307d4 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -17,7 +17,8 @@ namespace Jellyfin.Server.Extensions // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. const string specEndpoint = "/swagger/v1/swagger.json"; - return applicationBuilder.UseSwagger() + return applicationBuilder + .UseSwagger() .UseSwaggerUI(c => { c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); From 7c8188194b5bf9b74413f25d471a212f1677f7ed Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Sun, 19 Apr 2020 13:19:15 -0600 Subject: [PATCH 32/97] Address PR comments, and revert changes that changed the API schema --- .../Controllers/NotificationsController.cs | 20 +++++++++--------- .../NotificationDtos/NotificationDto.cs | 6 +++--- .../NotificationDtos/NotificationResultDto.cs | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c8a5be89b3..d9a5c5e316 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -42,13 +42,13 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - public IReadOnlyList GetNotifications( + public NotificationResultDto GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return new List(); + return new NotificationResultDto(); } /// @@ -92,10 +92,10 @@ namespace Jellyfin.Api.Controllers /// The level of the notification. [HttpPost("Admin")] public void CreateAdminNotification( - [FromForm] string name, - [FromForm] string description, - [FromForm] string? url, - [FromForm] NotificationLevel? level) + [FromQuery] string name, + [FromQuery] string description, + [FromQuery] string? url, + [FromQuery] NotificationLevel? level) { var notification = new NotificationRequest { @@ -114,11 +114,11 @@ namespace Jellyfin.Api.Controllers /// Endpoint to set notifications as read. /// /// The userID. - /// The IDs of notifications which should be set as read. + /// A comma-separated list of the IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] public void SetRead( [FromRoute] string userId, - [FromForm] List ids) + [FromQuery] string ids) { } @@ -126,11 +126,11 @@ namespace Jellyfin.Api.Controllers /// Endpoint to set notifications as unread. /// /// The userID. - /// The IDs of notifications which should be set as unread. + /// A comma-separated list of the IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] public void SetUnread( [FromRoute] string userId, - [FromForm] List ids) + [FromQuery] string ids) { } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs index c849ecd75d..502b22623b 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationDto.cs @@ -41,13 +41,13 @@ namespace Jellyfin.Api.Models.NotificationDtos public string Description { get; set; } = string.Empty; /// - /// Gets or sets the notification's URL. Defaults to null. + /// Gets or sets the notification's URL. Defaults to an empty string. /// - public string? Url { get; set; } + public string Url { get; set; } = string.Empty; /// /// Gets or sets the notification level. /// - public NotificationLevel? Level { get; set; } + public NotificationLevel Level { get; set; } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs new file mode 100644 index 0000000000..64e92bd83a --- /dev/null +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Api.Models.NotificationDtos +{ + /// + /// A list of notifications with the total record count for pagination. + /// + public class NotificationResultDto + { + /// + /// Gets or sets the current page of notifications. + /// + public IReadOnlyList Notifications { get; set; } = Array.Empty(); + + /// + /// Gets or sets the total number of notifications. + /// + public int TotalRecordCount { get; set; } + } +} From d30fd3b3d2e935f865e7560629d72e87cb0c760d Mon Sep 17 00:00:00 2001 From: randrey Date: Sun, 19 Apr 2020 14:14:04 -0700 Subject: [PATCH 33/97] Changed '\d' to '[0-9]'. --- Emby.Server.Implementations/Library/PathExtensions.cs | 2 +- MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 67c0e4eae7..1d61ed57eb 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attrib, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt\\d{7,8}", RegexOptions.IgnoreCase); + var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); return m.Success ? m.Value : null; } diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 0ac94834ea..5c8de80f17 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -209,7 +209,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits - var m = Regex.Match(xml, @"tt(\d{7,8})", RegexOptions.IgnoreCase); + var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); if (m.Success) { item.SetProviderId(MetadataProviders.Imdb, m.Value); From 6039c6200f78e6ae174d33efc3c3437cb94fa633 Mon Sep 17 00:00:00 2001 From: Mark Monteiro Date: Sun, 19 Apr 2020 18:18:53 -0400 Subject: [PATCH 34/97] Update instructions for running with the dotnet cli --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6007aa3cb9..7a81db6552 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,8 @@ After the required extensions are installed, you can can run the server by press To run the server from the command line you can use the `dotnet run` command. The example below shows how to do this if you have cloned the repository into a directory named `jellyfin` (the default directory name) and should work on all operating systems. ```bash -cd jellyfin # Move into the repository directory -cd Jellyfin.Server # Move into the server startup project directory -dotnet run # Run the server startup project +cd jellyfin # Move into the repository directory +dotnet run --project Jellyfin.Server # Run the server startup project ``` A second option is to build the project and then run the resulting executable file directly. When running the executable directly you can easily add command line options. Add the `--help` flag to list details on all the supported command line options. From ad67081840ec61085673634795d0b6363f649dbf Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 18:04:36 -0600 Subject: [PATCH 35/97] add camelCase formatter --- .../CamelCaseJsonProfileFormatter.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs new file mode 100644 index 0000000000..433a3197d3 --- /dev/null +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -0,0 +1,21 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// + /// Camel Case Json Profile Formatter. + /// + public class CamelCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// + /// Initializes a new instance of the class. + /// + public CamelCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) + { + SupportedMediaTypes.Clear(); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); + } + } +} From c89dc8921ffb0ce11031e9cfb096b525d94e21b3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 18:10:59 -0600 Subject: [PATCH 36/97] Fix PascalCase --- .../ApiServiceCollectionExtensions.cs | 3 +++ .../PascalCaseJsonProfileFormatter.cs | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 71ef9a69a2..00688074f0 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; +using Jellyfin.Server.Formatters; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -66,6 +67,8 @@ namespace Jellyfin.Server.Extensions return serviceCollection.AddMvc(opts => { opts.UseGeneralRoutePrefix(baseUrl); + opts.OutputFormatters.Insert(0, new CamelCaseJsonProfileFormatter()); + opts.OutputFormatters.Insert(0, new PascalCaseJsonProfileFormatter()); }) // Clear app parts to avoid other assemblies being picked up diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs new file mode 100644 index 0000000000..2ed006a336 --- /dev/null +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -0,0 +1,23 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace Jellyfin.Server.Formatters +{ + /// + /// Pascal Case Json Profile Formatter. + /// + public class PascalCaseJsonProfileFormatter : SystemTextJsonOutputFormatter + { + /// + /// Initializes a new instance of the class. + /// + public PascalCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = null }) + { + SupportedMediaTypes.Clear(); + // Add application/json for default formatter + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"PascalCase\"")); + } + } +} From 42781c4d4b54a010a185c71616090724f985631c Mon Sep 17 00:00:00 2001 From: Jay-Jay Date: Sun, 19 Apr 2020 22:15:33 +0000 Subject: [PATCH 37/97] Translated using Weblate (German) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/de/ --- Emby.Server.Implementations/Localization/Core/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 414430ff7b..82df43be11 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -3,7 +3,7 @@ "AppDeviceValues": "App: {0}, Gerät: {1}", "Application": "Anwendung", "Artists": "Interpreten", - "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifziert", + "AuthenticationSucceededWithUserName": "{0} hat sich erfolgreich authentifiziert", "Books": "Bücher", "CameraImageUploadedFrom": "Ein neues Foto wurde von {0} hochgeladen", "Channels": "Kanäle", @@ -99,11 +99,11 @@ "TaskRefreshChannels": "Erneuere Kanäle", "TaskCleanTranscodeDescription": "Löscht Transkodierdateien welche älter als ein Tag sind.", "TaskCleanTranscode": "Lösche Transkodier Pfad", - "TaskUpdatePluginsDescription": "Läd Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", + "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", "TaskUpdatePlugins": "Update Plugins", "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.", "TaskRefreshPeople": "Erneuere Schausteller", - "TaskCleanLogsDescription": "Lösche Log Datein die älter als {0} Tage sind.", + "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.", "TaskCleanLogs": "Lösche Log Pfad", "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", "TaskRefreshLibrary": "Scanne alle Bibliotheken", From 21b54b4ad8477d654e4f79e9805701c9737346a6 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 19:33:55 -0600 Subject: [PATCH 38/97] Move DeviceService to DevicesController --- Jellyfin.Api/Controllers/DevicesController.cs | 260 ++++++++++++++++++ MediaBrowser.Api/Devices/DeviceService.cs | 168 ----------- 2 files changed, 260 insertions(+), 168 deletions(-) create mode 100644 Jellyfin.Api/Controllers/DevicesController.cs delete mode 100644 MediaBrowser.Api/Devices/DeviceService.cs diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs new file mode 100644 index 0000000000..7407c44878 --- /dev/null +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -0,0 +1,260 @@ +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Devices; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Devices Controller. + /// + [Authenticated] + public class DevicesController : BaseJellyfinApiController + { + private readonly IDeviceManager _deviceManager; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly ISessionManager _sessionManager; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of interface. + /// Instance of interface. + /// Instance of interface. + public DevicesController( + IDeviceManager deviceManager, + IAuthenticationRepository authenticationRepository, + ISessionManager sessionManager) + { + _deviceManager = deviceManager; + _authenticationRepository = authenticationRepository; + _sessionManager = sessionManager; + } + + /// + /// Get Devices. + /// + /// /// Gets or sets a value indicating whether [supports synchronize]. + /// /// Gets or sets the user identifier. + /// Device Infos. + [HttpGet] + [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + { + try + { + var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; + var devices = _deviceManager.GetDevices(deviceQuery); + return Ok(devices); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get info for a device. + /// + /// Device Id. + /// Device Info. + [HttpGet("Info")] + [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) + { + try + { + var deviceInfo = _deviceManager.GetDevice(id); + if (deviceInfo == null) + { + return NotFound(); + } + + return Ok(deviceInfo); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Get options for a device. + /// + /// Device Id. + /// Device Info. + [HttpGet("Options")] + [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + { + try + { + var deviceInfo = _deviceManager.GetDeviceOptions(id); + if (deviceInfo == null) + { + return NotFound(); + } + + return Ok(deviceInfo); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Update device options. + /// + /// Device Id. + /// Device Options. + /// Status. + [HttpPost("Options")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult UpdateDeviceOptions( + [FromQuery, BindRequired] string id, + [FromBody, BindRequired] DeviceOptions deviceOptions) + { + try + { + var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); + if (existingDeviceOptions == null) + { + return NotFound(); + } + + _deviceManager.UpdateDeviceOptions(id, deviceOptions); + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Deletes a device. + /// + /// Device Id. + /// Status. + [HttpDelete] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult DeleteDevice([FromQuery, BindRequired] string id) + { + try + { + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; + + foreach (var session in sessions) + { + _sessionManager.Logout(session); + } + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Gets camera upload history for a device. + /// + /// Device Id. + /// Content Upload History. + [HttpGet("CameraUploads")] + [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) + { + try + { + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); + return Ok(uploadHistory); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + + /// + /// Uploads content. + /// + /// Device Id. + /// Album. + /// Name. + /// Id. + /// Status. + [HttpPost("CameraUploads")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public async Task PostCameraUploadAsync( + [FromQuery, BindRequired] string deviceId, + [FromQuery, BindRequired] string album, + [FromQuery, BindRequired] string name, + [FromQuery, BindRequired] string id) + { + try + { + Stream fileStream; + string contentType; + + if (Request.HasFormContentType) + { + if (Request.Form.Files.Any()) + { + fileStream = Request.Form.Files[0].OpenReadStream(); + contentType = Request.Form.Files[0].ContentType; + } + else + { + return BadRequest(); + } + } + else + { + fileStream = Request.Body; + contentType = Request.ContentType; + } + + await _deviceManager.AcceptCameraUpload( + deviceId, + fileStream, + new LocalFileInfo + { + MimeType = contentType, + Album = album, + Name = name, + Id = id + }).ConfigureAwait(false); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } + } + } +} diff --git a/MediaBrowser.Api/Devices/DeviceService.cs b/MediaBrowser.Api/Devices/DeviceService.cs deleted file mode 100644 index 7004a2559e..0000000000 --- a/MediaBrowser.Api/Devices/DeviceService.cs +++ /dev/null @@ -1,168 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api.Devices -{ - [Route("/Devices", "GET", Summary = "Gets all devices")] - [Authenticated(Roles = "Admin")] - public class GetDevices : DeviceQuery, IReturn> - { - } - - [Route("/Devices/Info", "GET", Summary = "Gets info for a device")] - [Authenticated(Roles = "Admin")] - public class GetDeviceInfo : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Devices/Options", "GET", Summary = "Gets options for a device")] - [Authenticated(Roles = "Admin")] - public class GetDeviceOptions : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string Id { get; set; } - } - - [Route("/Devices", "DELETE", Summary = "Deletes a device")] - public class DeleteDevice - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string Id { get; set; } - } - - [Route("/Devices/CameraUploads", "GET", Summary = "Gets camera upload history for a device")] - [Authenticated] - public class GetCameraUploads : IReturn - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] - public string DeviceId { get; set; } - } - - [Route("/Devices/CameraUploads", "POST", Summary = "Uploads content")] - [Authenticated] - public class PostCameraUpload : IRequiresRequestStream, IReturnVoid - { - [ApiMember(Name = "DeviceId", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string DeviceId { get; set; } - - [ApiMember(Name = "Album", Description = "Album", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Album { get; set; } - - [ApiMember(Name = "Name", Description = "Name", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Name { get; set; } - - [ApiMember(Name = "Id", Description = "Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "POST")] - public string Id { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/Devices/Options", "POST", Summary = "Updates device options")] - [Authenticated(Roles = "Admin")] - public class PostDeviceOptions : DeviceOptions, IReturnVoid - { - [ApiMember(Name = "Id", Description = "Device Id", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "DELETE")] - public string Id { get; set; } - } - - public class DeviceService : BaseApiService - { - private readonly IDeviceManager _deviceManager; - private readonly IAuthenticationRepository _authRepo; - private readonly ISessionManager _sessionManager; - - public DeviceService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IDeviceManager deviceManager, - IAuthenticationRepository authRepo, - ISessionManager sessionManager) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _deviceManager = deviceManager; - _authRepo = authRepo; - _sessionManager = sessionManager; - } - - public void Post(PostDeviceOptions request) - { - _deviceManager.UpdateDeviceOptions(request.Id, request); - } - - public object Get(GetDevices request) - { - return ToOptimizedResult(_deviceManager.GetDevices(request)); - } - - public object Get(GetDeviceInfo request) - { - return _deviceManager.GetDevice(request.Id); - } - - public object Get(GetDeviceOptions request) - { - return _deviceManager.GetDeviceOptions(request.Id); - } - - public object Get(GetCameraUploads request) - { - return ToOptimizedResult(_deviceManager.GetCameraUploadHistory(request.DeviceId)); - } - - public void Delete(DeleteDevice request) - { - var sessions = _authRepo.Get(new AuthenticationInfoQuery - { - DeviceId = request.Id - - }).Items; - - foreach (var session in sessions) - { - _sessionManager.Logout(session); - } - } - - public Task Post(PostCameraUpload request) - { - var deviceId = Request.QueryString["DeviceId"]; - var album = Request.QueryString["Album"]; - var id = Request.QueryString["Id"]; - var name = Request.QueryString["Name"]; - var req = Request.Response.HttpContext.Request; - - if (req.HasFormContentType) - { - var file = req.Form.Files.Count == 0 ? null : req.Form.Files[0]; - - return _deviceManager.AcceptCameraUpload(deviceId, file.OpenReadStream(), new LocalFileInfo - { - MimeType = file.ContentType, - Album = album, - Name = name, - Id = id - }); - } - - return _deviceManager.AcceptCameraUpload(deviceId, request.RequestStream, new LocalFileInfo - { - MimeType = Request.ContentType, - Album = album, - Name = name, - Id = id - }); - } - } -} From 440f060da6cfa8336d51bd05b723d67cfcf168eb Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 19 Apr 2020 19:36:18 -0600 Subject: [PATCH 39/97] Fix Authenticated Roles --- Jellyfin.Api/Controllers/DevicesController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 7407c44878..a9dcfb955a 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -48,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets or sets the user identifier. /// Device Infos. [HttpGet] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) @@ -70,6 +71,7 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Device Info. [HttpGet("Info")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] @@ -97,6 +99,7 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Device Info. [HttpGet("Options")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] @@ -125,6 +128,7 @@ namespace Jellyfin.Api.Controllers /// Device Options. /// Status. [HttpPost("Options")] + [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] From 16cae23bbee14a7398d39014973b1a476e1ca57c Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Sun, 19 Apr 2020 21:06:28 -0600 Subject: [PATCH 40/97] Add response type annotations, return IActionResult to handle errors --- .../Controllers/NotificationsController.cs | 79 ++++++++++++++----- 1 file changed, 59 insertions(+), 20 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index d9a5c5e316..76b025fa16 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -10,6 +10,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Notifications; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Notifications; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -42,13 +43,14 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - public NotificationResultDto GetNotifications( + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public IActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return new NotificationResultDto(); + return Ok(new NotificationResultDto()); } /// @@ -57,10 +59,11 @@ namespace Jellyfin.Api.Controllers /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] - public NotificationsSummaryDto GetNotificationsSummary( + [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] + public IActionResult GetNotificationsSummary( [FromRoute] string userId) { - return new NotificationsSummaryDto(); + return Ok(new NotificationsSummaryDto()); } /// @@ -68,9 +71,18 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - public IEnumerable GetNotificationTypes() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetNotificationTypes() { - return _notificationManager.GetNotificationTypes(); + try + { + return Ok(_notificationManager.GetNotificationTypes()); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -78,9 +90,18 @@ namespace Jellyfin.Api.Controllers /// /// All notification services. [HttpGet("Services")] - public IEnumerable GetNotificationServices() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult GetNotificationServices() { - return _notificationManager.GetNotificationServices(); + try + { + return Ok(_notificationManager.GetNotificationServices()); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -90,24 +111,36 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// Status. [HttpPost("Admin")] - public void CreateAdminNotification( + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] + public IActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, [FromQuery] NotificationLevel? level) { - var notification = new NotificationRequest + try { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), - Date = DateTime.UtcNow, - }; + var notification = new NotificationRequest + { + Name = name, + Description = description, + Url = url, + Level = level ?? NotificationLevel.Normal, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; - _notificationManager.SendNotification(notification, CancellationToken.None); + _notificationManager.SendNotification(notification, CancellationToken.None); + + return Ok(); + } + catch (Exception e) + { + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } /// @@ -115,11 +148,14 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. + /// Status. [HttpPost("{UserID}/Read")] - public void SetRead( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } /// @@ -127,11 +163,14 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. + /// Status. [HttpPost("{UserID}/Unread")] - public void SetUnread( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } } } From e6ef680775be122c3fcfbe64d4b8d868197b6f5e Mon Sep 17 00:00:00 2001 From: dkanada Date: Mon, 20 Apr 2020 14:27:46 +0900 Subject: [PATCH 41/97] add code suggestions --- MediaBrowser.Common/Extensions/BaseExtensions.cs | 1 + MediaBrowser.Model/Updates/VersionInfo.cs | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/MediaBrowser.Common/Extensions/BaseExtensions.cs b/MediaBrowser.Common/Extensions/BaseExtensions.cs index bc002e5233..08964420e7 100644 --- a/MediaBrowser.Common/Extensions/BaseExtensions.cs +++ b/MediaBrowser.Common/Extensions/BaseExtensions.cs @@ -35,6 +35,7 @@ namespace MediaBrowser.Common.Extensions { return new Guid(provider.ComputeHash(Encoding.Unicode.GetBytes(str))); } + #pragma warning restore CA5351 } } diff --git a/MediaBrowser.Model/Updates/VersionInfo.cs b/MediaBrowser.Model/Updates/VersionInfo.cs index e67e60d745..fe5826ad2f 100644 --- a/MediaBrowser.Model/Updates/VersionInfo.cs +++ b/MediaBrowser.Model/Updates/VersionInfo.cs @@ -1,6 +1,3 @@ -#pragma warning disable CS1591 -#pragma warning disable SA1600 - using System; namespace MediaBrowser.Model.Updates From 688240151bae0f333cd329572b3774954d13ebae Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 00:00:00 -0600 Subject: [PATCH 42/97] Enable nullable reference types on new class, remove unnecessary documenation and return types --- Jellyfin.Api/Controllers/NotificationsController.cs | 11 ++--------- .../Models/NotificationDtos/NotificationResultDto.cs | 2 ++ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 76b025fa16..c0c2be626b 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -72,7 +72,6 @@ namespace Jellyfin.Api.Controllers /// All notification types. [HttpGet("Types")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetNotificationTypes() { try @@ -91,7 +90,6 @@ namespace Jellyfin.Api.Controllers /// All notification services. [HttpGet("Services")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetNotificationServices() { try @@ -114,7 +112,6 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, @@ -148,14 +145,12 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. - /// Status. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetRead( + public void SetRead( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); } /// @@ -163,14 +158,12 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. - /// Status. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetUnread( + public void SetUnread( [FromRoute] string userId, [FromQuery] string ids) { - return Ok(); } } } diff --git a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs index 64e92bd83a..e34e176cb9 100644 --- a/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs +++ b/Jellyfin.Api/Models/NotificationDtos/NotificationResultDto.cs @@ -1,3 +1,5 @@ +#nullable enable + using System; using System.Collections.Generic; From ed1dc5c9222fdd1211caca2463f8bf5d6c4bb27d Mon Sep 17 00:00:00 2001 From: Anthony Lavado Date: Mon, 20 Apr 2020 02:35:47 -0400 Subject: [PATCH 43/97] Remove JsonIgnore from the DateLastSaved property of BaseItem --- MediaBrowser.Controller/Entities/BaseItem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index b9e1fe79d5..7ed8fa7671 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -549,7 +549,6 @@ namespace MediaBrowser.Controller.Entities [JsonIgnore] public DateTime DateModified { get; set; } - [JsonIgnore] public DateTime DateLastSaved { get; set; } [JsonIgnore] From 32ccab32bf49cd87a13aa3e6344a0736c51b0ff7 Mon Sep 17 00:00:00 2001 From: ejalal Date: Mon, 20 Apr 2020 11:05:48 +0000 Subject: [PATCH 44/97] Translated using Weblate (French) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fr/ --- .../Localization/Core/fr.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index ec29d33748..150952d8ba 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -11,7 +11,7 @@ "Collections": "Collections", "DeviceOfflineWithName": "{0} s'est déconnecté", "DeviceOnlineWithName": "{0} est connecté", - "FailedLoginAttemptWithUserName": "Échec de connexion de {0}", + "FailedLoginAttemptWithUserName": "Échec de connexion depuis {0}", "Favorites": "Favoris", "Folders": "Dossiers", "Genres": "Genres", @@ -69,7 +69,7 @@ "PluginUpdatedWithName": "{0} a été mis à jour", "ProviderValue": "Fournisseur : {0}", "ScheduledTaskFailedWithName": "{0} a échoué", - "ScheduledTaskStartedWithName": "{0} a commencé", + "ScheduledTaskStartedWithName": "{0} a démarré", "ServerNameNeedsToBeRestarted": "{0} doit être redémarré", "Shows": "Émissions", "Songs": "Chansons", @@ -95,21 +95,21 @@ "VersionNumber": "Version {0}", "TasksChannelsCategory": "Chaines en ligne", "TaskDownloadMissingSubtitlesDescription": "Cherche les sous-titres manquant sur internet en se basant sur la configuration des métadonnées.", - "TaskDownloadMissingSubtitles": "Télécharge les sous-titres manquant", + "TaskDownloadMissingSubtitles": "Télécharger les sous-titres manquant", "TaskRefreshChannelsDescription": "Rafraîchit les informations des chaines en ligne.", - "TaskRefreshChannels": "Rafraîchit les chaines", + "TaskRefreshChannels": "Rafraîchir les chaines", "TaskCleanTranscodeDescription": "Supprime les fichiers transcodés de plus d'un jour.", - "TaskCleanTranscode": "Nettoie les dossier des transcodages", - "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des plugins configurés pour être mis à jour automatiquement.", - "TaskUpdatePlugins": "Mettre à jour les plugins", - "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et directeurs dans votre bibliothèque.", - "TaskRefreshPeople": "Rafraîchit les acteurs", + "TaskCleanTranscode": "Nettoyer les dossier des transcodages", + "TaskUpdatePluginsDescription": "Télécharge et installe les mises à jours des extensions configurés pour être mises à jour automatiquement.", + "TaskUpdatePlugins": "Mettre à jour les extensions", + "TaskRefreshPeopleDescription": "Met à jour les métadonnées pour les acteurs et réalisateurs dans votre bibliothèque.", + "TaskRefreshPeople": "Rafraîchir les acteurs", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", - "TaskCleanLogs": "Nettoie le répertoire des journaux", + "TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskRefreshLibraryDescription": "Scanne toute les bibliothèques pour trouver les nouveaux fichiers et rafraîchit les métadonnées.", - "TaskRefreshLibrary": "Scanne toute les Bibliothèques", + "TaskRefreshLibrary": "Scanner toute les Bibliothèques", "TaskRefreshChapterImagesDescription": "Crée des images de miniature pour les vidéos ayant des chapitres.", - "TaskRefreshChapterImages": "Extrait les images de chapitre", + "TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCache": "Vider le répertoire cache", "TasksApplicationCategory": "Application", From fff2a40ffc4e5010b26143185c68d221225c1a22 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 20 Apr 2020 07:24:13 -0600 Subject: [PATCH 45/97] Remove StringEnumConverter --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a4f078b5b3..92bacb4400 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -80,9 +80,6 @@ namespace Jellyfin.Server.Extensions { // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. options.JsonSerializerOptions.PropertyNamingPolicy = null; - - // Accept string enums - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }) .AddControllersAsServices(); } From 67efcbee05fe7917aaff11fd27235fb952938434 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 20:16:58 -0600 Subject: [PATCH 46/97] Remove error handlers, to be implemented at a global level in a separate PR --- .../Controllers/NotificationsController.cs | 47 +++++-------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c0c2be626b..2a41f6020e 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -74,14 +74,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public IActionResult GetNotificationTypes() { - try - { - return Ok(_notificationManager.GetNotificationTypes()); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(_notificationManager.GetNotificationTypes()); } /// @@ -92,14 +85,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public IActionResult GetNotificationServices() { - try - { - return Ok(_notificationManager.GetNotificationServices()); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(_notificationManager.GetNotificationServices()); } /// @@ -112,32 +98,23 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult CreateAdminNotification( + public void CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, [FromQuery] NotificationLevel? level) { - try + var notification = new NotificationRequest { - var notification = new NotificationRequest - { - Name = name, - Description = description, - Url = url, - Level = level ?? NotificationLevel.Normal, - UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), - Date = DateTime.UtcNow, - }; + Name = name, + Description = description, + Url = url, + Level = level ?? NotificationLevel.Normal, + UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(), + Date = DateTime.UtcNow, + }; - _notificationManager.SendNotification(notification, CancellationToken.None); - - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + _notificationManager.SendNotification(notification, CancellationToken.None); } /// From 6c8e1d37bd49339d298c46c24cddf8e858b334c8 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Mon, 20 Apr 2020 23:53:09 -0600 Subject: [PATCH 47/97] Remove more unnecessary IActionResult --- .../Controllers/NotificationsController.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 2a41f6020e..932b91d55c 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -43,14 +43,14 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotifications( + [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)] + public NotificationResultDto GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, [FromQuery] int? limit) { - return Ok(new NotificationResultDto()); + return new NotificationResultDto(); } /// @@ -60,10 +60,10 @@ namespace Jellyfin.Api.Controllers /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] - public IActionResult GetNotificationsSummary( + public NotificationsSummaryDto GetNotificationsSummary( [FromRoute] string userId) { - return Ok(new NotificationsSummaryDto()); + return new NotificationsSummaryDto(); } /// @@ -71,10 +71,10 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotificationTypes() + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public IEnumerable GetNotificationTypes() { - return Ok(_notificationManager.GetNotificationTypes()); + return _notificationManager.GetNotificationTypes(); } /// @@ -83,9 +83,9 @@ namespace Jellyfin.Api.Controllers /// All notification services. [HttpGet("Services")] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IActionResult GetNotificationServices() + public IEnumerable GetNotificationServices() { - return Ok(_notificationManager.GetNotificationServices()); + return _notificationManager.GetNotificationServices(); } /// From dae69657108f90de54166a670c47a6dff2dae139 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 21 Apr 2020 00:24:35 -0600 Subject: [PATCH 48/97] Remove documentation of void return type --- Jellyfin.Api/Controllers/NotificationsController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 932b91d55c..c1d9e32515 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -95,7 +95,6 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. - /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public void CreateAdminNotification( From 1175ce3f97fdebc6fdb489ce65deaac59c7b7f87 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:36:22 -0600 Subject: [PATCH 49/97] Add Exception Middleware --- .../Models/ExceptionDtos/ExceptionDto.cs | 14 +++++ .../ApiApplicationBuilderExtensions.cs | 11 ++++ .../Middleware/ExceptionMiddleware.cs | 60 +++++++++++++++++++ Jellyfin.Server/Startup.cs | 2 + 4 files changed, 87 insertions(+) create mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs create mode 100644 Jellyfin.Server/Middleware/ExceptionMiddleware.cs diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs new file mode 100644 index 0000000000..d2b48d4ae5 --- /dev/null +++ b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs @@ -0,0 +1,14 @@ +namespace Jellyfin.Api.Models.ExceptionDtos +{ + /// + /// Exception Dto. + /// Used for graceful handling of API exceptions. + /// + public class ExceptionDto + { + /// + /// Gets or sets exception message. + /// + public string Message { get; set; } + } +} diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index db06eb4552..6c105ab65b 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Jellyfin.Server.Middleware; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -23,5 +24,15 @@ namespace Jellyfin.Server.Extensions c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); }); } + + /// + /// Adds exception middleware to the application pipeline. + /// + /// The application builder. + /// The updated application builder. + public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder) + { + return applicationBuilder.UseMiddleware(); + } } } diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000000..39aace95d2 --- /dev/null +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,60 @@ +using System; +using System.Text.Json; +using System.Threading.Tasks; +using Jellyfin.Api.Models.ExceptionDtos; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Server.Middleware +{ + /// + /// Exception Middleware. + /// + public class ExceptionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Next request delegate. + /// Instance of the interface. + public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = loggerFactory.CreateLogger() ?? + throw new ArgumentNullException(nameof(loggerFactory)); + } + + /// + /// Invoke request. + /// + /// Request context. + /// Task. + public async Task Invoke(HttpContext context) + { + try + { + await _next(context).ConfigureAwait(false); + } + catch (Exception ex) + { + if (context.Response.HasStarted) + { + _logger.LogWarning("The response has already started, the exception middleware will not be executed."); + throw; + } + + var exceptionBody = new ExceptionDto { Message = ex.Message }; + var exceptionJson = JsonSerializer.Serialize(exceptionBody); + + context.Response.Clear(); + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + // TODO switch between PascalCase and camelCase + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false); + } + } + } +} diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d4..7a632f6c44 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -58,6 +58,8 @@ namespace Jellyfin.Server app.UseDeveloperExceptionPage(); } + app.UseExceptionMiddleware(); + app.UseWebSockets(); app.UseResponseCompression(); From 5ef71d592b84b73290e3e7a34cd7fa8b9f337f50 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 07:55:01 -0600 Subject: [PATCH 50/97] Remove exception handler --- Jellyfin.Api/Controllers/DevicesController.cs | 143 ++++++------------ 1 file changed, 44 insertions(+), 99 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index a9dcfb955a..5dc3f27ee1 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -53,16 +53,9 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { - try - { - var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - var devices = _deviceManager.GetDevices(deviceQuery); - return Ok(devices); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; + var devices = _deviceManager.GetDevices(deviceQuery); + return Ok(devices); } /// @@ -77,20 +70,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) { - try + var deviceInfo = _deviceManager.GetDevice(id); + if (deviceInfo == null) { - var deviceInfo = _deviceManager.GetDevice(id); - if (deviceInfo == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(deviceInfo); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(deviceInfo); } /// @@ -105,20 +91,13 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { - try + var deviceInfo = _deviceManager.GetDeviceOptions(id); + if (deviceInfo == null) { - var deviceInfo = _deviceManager.GetDeviceOptions(id); - if (deviceInfo == null) - { - return NotFound(); - } + return NotFound(); + } - return Ok(deviceInfo); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + return Ok(deviceInfo); } /// @@ -136,21 +115,14 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string id, [FromBody, BindRequired] DeviceOptions deviceOptions) { - try + var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); + if (existingDeviceOptions == null) { - var existingDeviceOptions = _deviceManager.GetDeviceOptions(id); - if (existingDeviceOptions == null) - { - return NotFound(); - } + return NotFound(); + } - _deviceManager.UpdateDeviceOptions(id, deviceOptions); - return Ok(); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + _deviceManager.UpdateDeviceOptions(id, deviceOptions); + return Ok(); } /// @@ -163,21 +135,14 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult DeleteDevice([FromQuery, BindRequired] string id) { - try - { - var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; - foreach (var session in sessions) - { - _sessionManager.Logout(session); - } - - return Ok(); - } - catch (Exception e) + foreach (var session in sessions) { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + _sessionManager.Logout(session); } + + return Ok(); } /// @@ -190,15 +155,8 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) { - try - { - var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return Ok(uploadHistory); - } - catch (Exception e) - { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); - } + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); + return Ok(uploadHistory); } /// @@ -219,46 +177,33 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string name, [FromQuery, BindRequired] string id) { - try - { - Stream fileStream; - string contentType; + Stream fileStream; + string contentType; - if (Request.HasFormContentType) + if (Request.HasFormContentType) + { + if (Request.Form.Files.Any()) { - if (Request.Form.Files.Any()) - { - fileStream = Request.Form.Files[0].OpenReadStream(); - contentType = Request.Form.Files[0].ContentType; - } - else - { - return BadRequest(); - } + fileStream = Request.Form.Files[0].OpenReadStream(); + contentType = Request.Form.Files[0].ContentType; } else { - fileStream = Request.Body; - contentType = Request.ContentType; + return BadRequest(); } - - await _deviceManager.AcceptCameraUpload( - deviceId, - fileStream, - new LocalFileInfo - { - MimeType = contentType, - Album = album, - Name = name, - Id = id - }).ConfigureAwait(false); - - return Ok(); } - catch (Exception e) + else { - return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + fileStream = Request.Body; + contentType = Request.ContentType; } + + await _deviceManager.AcceptCameraUpload( + deviceId, + fileStream, + new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false); + + return Ok(); } } } From fe632146dcba69edeec56b850736227ff5f4c5b3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:17:13 -0600 Subject: [PATCH 51/97] Move Json Options to static class for easier access. --- .../CamelCaseJsonProfileFormatter.cs | 4 +- .../PascalCaseJsonProfileFormatter.cs | 4 +- Jellyfin.Server/Models/JsonOptions.cs | 41 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Jellyfin.Server/Models/JsonOptions.cs diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index 433a3197d3..e6ad6dfb13 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using Jellyfin.Server.Models; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public CamelCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) + public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase) { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 2ed006a336..675f4c79ee 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using Jellyfin.Server.Models; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public PascalCaseJsonProfileFormatter() : base(new JsonSerializerOptions { PropertyNamingPolicy = null }) + public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs new file mode 100644 index 0000000000..fa503bc9a4 --- /dev/null +++ b/Jellyfin.Server/Models/JsonOptions.cs @@ -0,0 +1,41 @@ +using System.Text.Json; + +namespace Jellyfin.Server.Models +{ + /// + /// Json Options. + /// + public static class JsonOptions + { + /// + /// Base Json Serializer Options. + /// + private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions(); + + /// + /// Gets CamelCase json options. + /// + public static JsonSerializerOptions CamelCase + { + get + { + var options = _jsonOptions; + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// + /// Gets PascalCase json options. + /// + public static JsonSerializerOptions PascalCase + { + get + { + var options = _jsonOptions; + options.PropertyNamingPolicy = null; + return options; + } + } + } +} From 14361c68cf71bc810d282901a764d2f8d5858eea Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:38:31 -0600 Subject: [PATCH 52/97] Add ProducesResponseType to base controller --- Jellyfin.Api/BaseJellyfinApiController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index 1f4508e6cb..f691759866 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,3 +1,5 @@ +using Jellyfin.Api.Models.ExceptionDtos; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -7,6 +9,7 @@ namespace Jellyfin.Api /// [ApiController] [Route("[controller]")] + [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)] public class BaseJellyfinApiController : ControllerBase { } From b8fd9c785e107b6d2ae8125d6e6b6374f36fe9a3 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 08:42:48 -0600 Subject: [PATCH 53/97] Convert StartupController to IActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index afc9b8f3da..b0b26c1762 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Models.StartupDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api.Controllers @@ -32,12 +33,15 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Status. [HttpPost("Complete")] - public void CompleteWizard() + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); _config.SaveConfiguration(); + return Ok(); } /// @@ -45,7 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - public StartupConfigurationDto GetStartupConfiguration() + [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] + public IActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -54,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return result; + return Ok(result); } /// @@ -63,8 +68,10 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Status. [HttpPost("Configuration")] - public void UpdateInitialConfiguration( + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -73,6 +80,7 @@ namespace Jellyfin.Api.Controllers _config.Configuration.MetadataCountryCode = metadataCountryCode; _config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage; _config.SaveConfiguration(); + return Ok(); } /// @@ -80,12 +88,15 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Status. [HttpPost("RemoteAccess")] - public void SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + [ProducesResponseType(StatusCodes.Status200OK)] + public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; _config.SaveConfiguration(); + return Ok(); } /// @@ -93,14 +104,11 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - public StartupUserDto GetFirstUser() + [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] + public IActionResult GetFirstUser() { var user = _userManager.Users.First(); - return new StartupUserDto - { - Name = user.Name, - Password = user.Password - }; + return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); } /// @@ -109,7 +117,8 @@ namespace Jellyfin.Api.Controllers /// The DTO containing username and password. /// The async task. [HttpPost("User")] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); @@ -121,6 +130,8 @@ namespace Jellyfin.Api.Controllers { await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false); } + + return Ok(); } } } From 3ef8448a518e673feae0c70c2682d60e4632c0cd Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 09:09:05 -0600 Subject: [PATCH 54/97] Return to previous exception handle implementation --- Jellyfin.Api/BaseJellyfinApiController.cs | 3 - .../Models/ExceptionDtos/ExceptionDto.cs | 14 --- .../Middleware/ExceptionMiddleware.cs | 86 ++++++++++++++++--- 3 files changed, 73 insertions(+), 30 deletions(-) delete mode 100644 Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs index f691759866..1f4508e6cb 100644 --- a/Jellyfin.Api/BaseJellyfinApiController.cs +++ b/Jellyfin.Api/BaseJellyfinApiController.cs @@ -1,5 +1,3 @@ -using Jellyfin.Api.Models.ExceptionDtos; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jellyfin.Api @@ -9,7 +7,6 @@ namespace Jellyfin.Api /// [ApiController] [Route("[controller]")] - [ProducesResponseType(typeof(ExceptionDto), StatusCodes.Status500InternalServerError)] public class BaseJellyfinApiController : ControllerBase { } diff --git a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs b/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs deleted file mode 100644 index d2b48d4ae5..0000000000 --- a/Jellyfin.Api/Models/ExceptionDtos/ExceptionDto.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Jellyfin.Api.Models.ExceptionDtos -{ - /// - /// Exception Dto. - /// Used for graceful handling of API exceptions. - /// - public class ExceptionDto - { - /// - /// Gets or sets exception message. - /// - public string Message { get; set; } - } -} diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 39aace95d2..0d9dac89f0 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,7 +1,9 @@ using System; -using System.Text.Json; +using System.IO; using System.Threading.Tasks; -using Jellyfin.Api.Models.ExceptionDtos; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -14,17 +16,22 @@ namespace Jellyfin.Server.Middleware { private readonly RequestDelegate _next; private readonly ILogger _logger; + private readonly IServerConfigurationManager _configuration; /// /// Initializes a new instance of the class. /// /// Next request delegate. /// Instance of the interface. - public ExceptionMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) + /// Instance of the interface. + public ExceptionMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IServerConfigurationManager serverConfigurationManager) { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _logger = loggerFactory.CreateLogger() ?? - throw new ArgumentNullException(nameof(loggerFactory)); + _next = next; + _logger = loggerFactory.CreateLogger(); + _configuration = serverConfigurationManager; } /// @@ -46,15 +53,68 @@ namespace Jellyfin.Server.Middleware throw; } - var exceptionBody = new ExceptionDto { Message = ex.Message }; - var exceptionJson = JsonSerializer.Serialize(exceptionBody); + ex = GetActualException(ex); + _logger.LogError(ex, "Error processing request: {0}", ex.Message); + context.Response.StatusCode = GetStatusCode(ex); + context.Response.ContentType = "text/plain"; - context.Response.Clear(); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - // TODO switch between PascalCase and camelCase - context.Response.ContentType = "application/json"; - await context.Response.WriteAsync(exceptionJson).ConfigureAwait(false); + var errorContent = NormalizeExceptionMessage(ex.Message); + await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } } + + private static Exception GetActualException(Exception ex) + { + if (ex is AggregateException agg) + { + var inner = agg.InnerException; + if (inner != null) + { + return GetActualException(inner); + } + + var inners = agg.InnerExceptions; + if (inners.Count > 0) + { + return GetActualException(inners[0]); + } + } + + return ex; + } + + private static int GetStatusCode(Exception ex) + { + switch (ex) + { + case ArgumentException _: return StatusCodes.Status400BadRequest; + case SecurityException _: return StatusCodes.Status401Unauthorized; + case DirectoryNotFoundException _: + case FileNotFoundException _: + case ResourceNotFoundException _: return StatusCodes.Status404NotFound; + case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; + default: return StatusCodes.Status500InternalServerError; + } + } + + private string NormalizeExceptionMessage(string msg) + { + if (msg == null) + { + return string.Empty; + } + + // Strip any information we don't want to reveal + msg = msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + msg = msg.Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); + + return msg; + } } } From 166a4e8129d51f508e0e8a7566381c22fa6cf239 Mon Sep 17 00:00:00 2001 From: Mehdi Khosravi Date: Tue, 21 Apr 2020 09:21:56 +0000 Subject: [PATCH 55/97] Translated using Weblate (Persian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fa/ --- Emby.Server.Implementations/Localization/Core/fa.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index be6f87ee3f..500c292170 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -23,7 +23,7 @@ "HeaderFavoriteEpisodes": "قسمت‌های مورد علاقه", "HeaderFavoriteShows": "سریال‌های مورد علاقه", "HeaderFavoriteSongs": "آهنگ‌های مورد علاقه", - "HeaderLiveTV": "تلویزیون زنده", + "HeaderLiveTV": "پخش زنده", "HeaderNextUp": "قسمت بعدی", "HeaderRecordingGroups": "گروه‌های ضبط", "HomeVideos": "ویدیوهای خانگی", From b88a94116bb7ac0857bffb38a3eb8bfe88a40d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= Date: Tue, 21 Apr 2020 09:20:22 +0000 Subject: [PATCH 56/97] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- .../Localization/Core/pt-PT.json | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index ebf35c4920..c1fb65743d 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -26,7 +26,7 @@ "HeaderLiveTV": "TV em Direto", "HeaderNextUp": "A Seguir", "HeaderRecordingGroups": "Grupos de Gravação", - "HomeVideos": "Home videos", + "HomeVideos": "Videos caseiros", "Inherit": "Herdar", "ItemAddedWithName": "{0} foi adicionado à biblioteca", "ItemRemovedWithName": "{0} foi removido da biblioteca", @@ -92,5 +92,27 @@ "UserStoppedPlayingItemWithValues": "{0} terminou a reprodução de {1} em {2}", "ValueHasBeenAddedToLibrary": "{0} foi adicionado à sua biblioteca multimédia", "ValueSpecialEpisodeName": "Especial - {0}", - "VersionNumber": "Versão {0}" + "VersionNumber": "Versão {0}", + "TaskDownloadMissingSubtitlesDescription": "Procurar na internet por legendas em falta baseado na configuração de metadados.", + "TaskDownloadMissingSubtitles": "Fazer download de legendas em falta", + "TaskRefreshChannelsDescription": "Atualizar informação sobre canais da Internet.", + "TaskRefreshChannels": "Atualizar Canais", + "TaskCleanTranscodeDescription": "Apagar ficheiros de transcode com mais de um dia.", + "TaskCleanTranscode": "Limpar a Diretoria de Transcode", + "TaskUpdatePluginsDescription": "Faz o download e instala updates para os plugins que estão configurados para atualizar automaticamente.", + "TaskUpdatePlugins": "Atualizar Plugins", + "TaskRefreshPeopleDescription": "Atualizar metadados para atores e diretores na biblioteca.", + "TaskRefreshPeople": "Atualizar Pessoas", + "TaskCleanLogsDescription": "Apagar ficheiros de log que têm mais de {0} dias.", + "TaskCleanLogs": "Limpar a Diretoria de Logs", + "TaskRefreshLibraryDescription": "Scannear a biblioteca de música para novos ficheiros e atualizar os metadados.", + "TaskRefreshLibrary": "Scannear Biblioteca de Música", + "TaskRefreshChapterImagesDescription": "Criar thumbnails para os vídeos que têm capítulos.", + "TaskRefreshChapterImages": "Extrair Imagens dos Capítulos", + "TaskCleanCacheDescription": "Apagar ficheiros em cache que já não são necessários.", + "TaskCleanCache": "Limpar Cache", + "TasksChannelsCategory": "Canais da Internet", + "TasksApplicationCategory": "Aplicação", + "TasksLibraryCategory": "Biblioteca", + "TasksMaintenanceCategory": "Manutenção" } From a2f19eadf739297cbbc99c9082b0175e8b881054 Mon Sep 17 00:00:00 2001 From: "Chen-Tai,Peng" Date: Tue, 21 Apr 2020 17:18:29 +0000 Subject: [PATCH 57/97] Translated using Weblate (Chinese (Traditional)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/zh_Hant/ --- .../Localization/Core/zh-TW.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index c423c7ea73..766e338dab 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -91,5 +91,18 @@ "VersionNumber": "版本 {0}", "HeaderRecordingGroups": "錄製組", "Inherit": "繼承", - "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕" + "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", + "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。", + "TaskDownloadMissingSubtitles": "下載遺失的字幕", + "TaskRefreshChannels": "重新整理頻道", + "TaskUpdatePlugins": "更新插件", + "TaskRefreshPeople": "重新整理人員", + "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔案。", + "TaskCleanLogs": "清空紀錄資料夾", + "TaskRefreshLibraryDescription": "掃描媒體庫內新的檔案並重新整理描述資料。", + "TaskRefreshLibrary": "掃描媒體庫", + "TaskRefreshChapterImages": "擷取章節圖片", + "TaskCleanCacheDescription": "刪除系統長時間不需要的快取。", + "TaskCleanCache": "清除快取資料夾", + "TasksLibraryCategory": "媒體庫" } From 69d9bfb233bd2716e3803b38c55275de58bb8d46 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 21 Apr 2020 12:10:34 -0600 Subject: [PATCH 58/97] Make documentation of parameters clearer Co-Authored-By: Vasily --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index c1d9e32515..6145246ed3 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -38,7 +38,7 @@ namespace Jellyfin.Api.Controllers /// Endpoint for getting a user's notifications. /// /// The user's ID. - /// An optional filter by IsRead. + /// An optional filter by notification read state. /// The optional index to start at. All notifications with a lower index will be dropped from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. From 927696c4036960018864864a4acbf0aeb797f7ac Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 13:59:43 -0600 Subject: [PATCH 59/97] move to ActionResult --- Jellyfin.Api/Controllers/DevicesController.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 5dc3f27ee1..559a260071 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -49,9 +49,8 @@ namespace Jellyfin.Api.Controllers /// Device Infos. [HttpGet] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceInfo[]), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; var devices = _deviceManager.GetDevices(deviceQuery); @@ -65,10 +64,9 @@ namespace Jellyfin.Api.Controllers /// Device Info. [HttpGet("Info")] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceInfo), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDeviceInfo([FromQuery, BindRequired] string id) + public ActionResult GetDeviceInfo([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDevice(id); if (deviceInfo == null) @@ -86,10 +84,9 @@ namespace Jellyfin.Api.Controllers /// Device Info. [HttpGet("Options")] [Authenticated(Roles = "Admin")] - [ProducesResponseType(typeof(DeviceOptions), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -110,8 +107,7 @@ namespace Jellyfin.Api.Controllers [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult UpdateDeviceOptions( + public ActionResult UpdateDeviceOptions( [FromQuery, BindRequired] string id, [FromBody, BindRequired] DeviceOptions deviceOptions) { @@ -132,8 +128,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult DeleteDevice([FromQuery, BindRequired] string id) + public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; @@ -151,9 +146,8 @@ namespace Jellyfin.Api.Controllers /// Device Id. /// Content Upload History. [HttpGet("CameraUploads")] - [ProducesResponseType(typeof(ContentUploadHistory), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public IActionResult GetCameraUploads([FromQuery, BindRequired] string id) + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { var uploadHistory = _deviceManager.GetCameraUploadHistory(id); return Ok(uploadHistory); @@ -170,8 +164,7 @@ namespace Jellyfin.Api.Controllers [HttpPost("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(string), StatusCodes.Status500InternalServerError)] - public async Task PostCameraUploadAsync( + public async Task PostCameraUploadAsync( [FromQuery, BindRequired] string deviceId, [FromQuery, BindRequired] string album, [FromQuery, BindRequired] string name, From 7db3b035a6d1f7e6f4886c4497b98b7a6af6c679 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 14:25:03 -0600 Subject: [PATCH 60/97] move to ActionResult --- Jellyfin.Api/Controllers/StartupController.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index b0b26c1762..2db7e32aad 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -36,7 +36,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult CompleteWizard() + public ActionResult CompleteWizard() { _config.Configuration.IsStartupWizardCompleted = true; _config.SetOptimalValues(); @@ -49,8 +49,8 @@ namespace Jellyfin.Api.Controllers /// /// The initial startup wizard configuration. [HttpGet("Configuration")] - [ProducesResponseType(typeof(StartupConfigurationDto), StatusCodes.Status200OK)] - public IActionResult GetStartupConfiguration() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetStartupConfiguration() { var result = new StartupConfigurationDto { @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult UpdateInitialConfiguration( + public ActionResult UpdateInitialConfiguration( [FromForm] string uiCulture, [FromForm] string metadataCountryCode, [FromForm] string preferredMetadataLanguage) @@ -91,7 +91,7 @@ namespace Jellyfin.Api.Controllers /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] - public IActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) + public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) { _config.Configuration.EnableRemoteAccess = enableRemoteAccess; _config.Configuration.EnableUPnP = enableAutomaticPortMapping; @@ -104,8 +104,8 @@ namespace Jellyfin.Api.Controllers /// /// The first user. [HttpGet("User")] - [ProducesResponseType(typeof(StartupUserDto), StatusCodes.Status200OK)] - public IActionResult GetFirstUser() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetFirstUser() { var user = _userManager.Users.First(); return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); @@ -118,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) + public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) { var user = _userManager.Users.First(); From 3ab61dbdc252670abf28797d3183614b1cd05ece Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 15:49:04 -0600 Subject: [PATCH 61/97] bump swashbuckle --- Jellyfin.Api/Jellyfin.Api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index cbb1d3007f..77bb52c6a5 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -10,8 +10,8 @@ - - + + From 2542a27bd5f79ccfbc2547ddd877ddb0423ae296 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 16:15:31 -0600 Subject: [PATCH 62/97] Fix documentation endpoint for use with baseUrl --- .../ApiApplicationBuilderExtensions.cs | 28 ++++++++++++++----- Jellyfin.Server/Startup.cs | 2 +- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 43c49307d4..df3bab931b 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -11,23 +12,36 @@ namespace Jellyfin.Server.Extensions /// Adds swagger and swagger UI to the application pipeline. /// /// The application builder. + /// The server configuration. /// The updated application builder. - public static IApplicationBuilder UseJellyfinApiSwagger(this IApplicationBuilder applicationBuilder) + public static IApplicationBuilder UseJellyfinApiSwagger( + this IApplicationBuilder applicationBuilder, + IServerConfigurationManager serverConfigurationManager) { // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), // specifying the Swagger JSON endpoint. - const string specEndpoint = "/swagger/v1/swagger.json"; + + var baseUrl = serverConfigurationManager.Configuration.BaseUrl.Trim('/'); + if (!string.IsNullOrEmpty(baseUrl)) + { + baseUrl += '/'; + } + return applicationBuilder - .UseSwagger() + .UseSwagger(c => + { + c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json"; + }) .UseSwaggerUI(c => { - c.SwaggerEndpoint(specEndpoint, "Jellyfin API V1"); - c.RoutePrefix = "api-docs/swagger"; + c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); + c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; }) .UseReDoc(c => { - c.SpecUrl(specEndpoint); - c.RoutePrefix = "api-docs/redoc"; + c.DocumentTitle = "Jellyfin API v1"; + c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json"); + c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc"; }); } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 4d7d56e9d4..ee08d2580a 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -66,7 +66,7 @@ namespace Jellyfin.Server app.Use(serverApplicationHost.ExecuteWebsocketHandlerAsync); // TODO use when old API is removed: app.UseAuthentication(); - app.UseJellyfinApiSwagger(); + app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => From 041d674eb6e4a675b68406ed5c2d7018d61e870a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 21 Apr 2020 16:19:26 -0600 Subject: [PATCH 63/97] Fix swagger ui Document Title --- Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index df3bab931b..d094242259 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -34,6 +34,7 @@ namespace Jellyfin.Server.Extensions }) .UseSwaggerUI(c => { + c.DocumentTitle = "Jellyfin API v1"; c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; }) From 2a49b19a7c02f16cd4bb1d847c1ff76c5df316fb Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 22 Apr 2020 00:21:37 -0600 Subject: [PATCH 64/97] Update documentation of startIndex Co-Authored-By: Vasily --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 6145246ed3..bb9f5a7b3c 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -39,7 +39,7 @@ namespace Jellyfin.Api.Controllers /// /// The user's ID. /// An optional filter by notification read state. - /// The optional index to start at. All notifications with a lower index will be dropped from the results. + /// The optional index to start at. All notifications with a lower index will be omitted from the results. /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] From 7693cc0db006ef4eb3a90d161b14ac4551bb96a7 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 22 Apr 2020 10:00:10 -0600 Subject: [PATCH 65/97] Use ActionResult return type for all endpoints --- .../Controllers/NotificationsController.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index bb9f5a7b3c..0bf3aa1b47 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -43,8 +43,8 @@ namespace Jellyfin.Api.Controllers /// An optional limit on the number of notifications returned. /// A read-only list of all of the user's notifications. [HttpGet("{UserID}")] - [ProducesResponseType(typeof(NotificationResultDto), StatusCodes.Status200OK)] - public NotificationResultDto GetNotifications( + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNotifications( [FromRoute] string userId, [FromQuery] bool? isRead, [FromQuery] int? startIndex, @@ -59,8 +59,8 @@ namespace Jellyfin.Api.Controllers /// The user's ID. /// Notifications summary for the user. [HttpGet("{UserID}/Summary")] - [ProducesResponseType(typeof(NotificationsSummaryDto), StatusCodes.Status200OK)] - public NotificationsSummaryDto GetNotificationsSummary( + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNotificationsSummary( [FromRoute] string userId) { return new NotificationsSummaryDto(); @@ -71,8 +71,8 @@ namespace Jellyfin.Api.Controllers /// /// All notification types. [HttpGet("Types")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IEnumerable GetNotificationTypes() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNotificationTypes() { return _notificationManager.GetNotificationTypes(); } @@ -82,10 +82,10 @@ namespace Jellyfin.Api.Controllers /// /// All notification services. [HttpGet("Services")] - [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] - public IEnumerable GetNotificationServices() + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult> GetNotificationServices() { - return _notificationManager.GetNotificationServices(); + return _notificationManager.GetNotificationServices().ToList(); } /// @@ -97,7 +97,7 @@ namespace Jellyfin.Api.Controllers /// The level of the notification. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public void CreateAdminNotification( + public ActionResult CreateAdminNotification( [FromQuery] string name, [FromQuery] string description, [FromQuery] string? url, @@ -114,6 +114,8 @@ namespace Jellyfin.Api.Controllers }; _notificationManager.SendNotification(notification, CancellationToken.None); + + return Ok(); } /// @@ -123,10 +125,11 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as read. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] - public void SetRead( + public ActionResult SetRead( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } /// @@ -136,10 +139,11 @@ namespace Jellyfin.Api.Controllers /// A comma-separated list of the IDs of notifications which should be set as unread. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] - public void SetUnread( + public ActionResult SetUnread( [FromRoute] string userId, [FromQuery] string ids) { + return Ok(); } } } From a06d271725f6e746d9a970f29283ab8f3ebae607 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 22 Apr 2020 13:07:21 -0600 Subject: [PATCH 66/97] Move ConfigurationService to Jellyfin.Api --- .../Controllers/ConfigurationController.cs | 128 +++++++++++++++ .../ConfigurationDtos/MediaEncoderPathDto.cs | 18 +++ MediaBrowser.Api/ConfigurationService.cs | 146 ------------------ 3 files changed, 146 insertions(+), 146 deletions(-) create mode 100644 Jellyfin.Api/Controllers/ConfigurationController.cs create mode 100644 Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs delete mode 100644 MediaBrowser.Api/ConfigurationService.cs diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs new file mode 100644 index 0000000000..14e45833f0 --- /dev/null +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -0,0 +1,128 @@ +#nullable enable + +using System.Threading.Tasks; +using Jellyfin.Api.Models.ConfigurationDtos; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Jellyfin.Api.Controllers +{ + /// + /// Configuration Controller. + /// + [Route("System")] + [Authenticated] + public class ConfigurationController : BaseJellyfinApiController + { + private readonly IServerConfigurationManager _configurationManager; + private readonly IMediaEncoder _mediaEncoder; + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Initializes a new instance of the class. + /// + /// Instance of the interface. + /// Instance of the interface. + /// Instance of the interface. + public ConfigurationController( + IServerConfigurationManager configurationManager, + IMediaEncoder mediaEncoder, + IJsonSerializer jsonSerializer) + { + _configurationManager = configurationManager; + _mediaEncoder = mediaEncoder; + _jsonSerializer = jsonSerializer; + } + + /// + /// Gets application configuration. + /// + /// Application configuration. + [HttpGet("Configuration")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetConfiguration() + { + return Ok(_configurationManager.Configuration); + } + + /// + /// Updates application configuration. + /// + /// Configuration. + /// Status. + [HttpPost("Configuration")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) + { + _configurationManager.ReplaceConfiguration(configuration); + return Ok(); + } + + /// + /// Gets a named configuration. + /// + /// Configuration key. + /// Configuration. + [HttpGet("Configuration/{Key}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetNamedConfiguration([FromRoute] string key) + { + return Ok(_configurationManager.GetConfiguration(key)); + } + + /// + /// Updates named configuration. + /// + /// Configuration key. + /// Status. + [HttpPost("Configuration/{Key}")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task UpdateNamedConfiguration([FromRoute] string key) + { + var configurationType = _configurationManager.GetConfigurationType(key); + /* + // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. + var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); + */ + + var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) + .ConfigureAwait(false); + _configurationManager.SaveConfiguration(key, configuration); + return Ok(); + } + + /// + /// Gets a default MetadataOptions object. + /// + /// MetadataOptions. + [HttpGet("Configuration/MetadataOptions/Default")] + [Authenticated(Roles = "Admin")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult GetDefaultMetadataOptions() + { + return Ok(new MetadataOptions()); + } + + /// + /// Updates the path to the media encoder. + /// + /// Media encoder path form body. + /// Status. + [HttpPost("MediaEncoder/Path")] + [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) + { + _mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType); + return Ok(); + } + } +} diff --git a/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs new file mode 100644 index 0000000000..b05e0cdf5a --- /dev/null +++ b/Jellyfin.Api/Models/ConfigurationDtos/MediaEncoderPathDto.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Api.Models.ConfigurationDtos +{ + /// + /// Media Encoder Path Dto. + /// + public class MediaEncoderPathDto + { + /// + /// Gets or sets media encoder path. + /// + public string Path { get; set; } + + /// + /// Gets or sets media encoder path type. + /// + public string PathType { get; set; } + } +} diff --git a/MediaBrowser.Api/ConfigurationService.cs b/MediaBrowser.Api/ConfigurationService.cs deleted file mode 100644 index 316be04a03..0000000000 --- a/MediaBrowser.Api/ConfigurationService.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace MediaBrowser.Api -{ - /// - /// Class GetConfiguration - /// - [Route("/System/Configuration", "GET", Summary = "Gets application configuration")] - [Authenticated] - public class GetConfiguration : IReturn - { - - } - - [Route("/System/Configuration/{Key}", "GET", Summary = "Gets a named configuration")] - [Authenticated(AllowBeforeStartupWizard = true)] - public class GetNamedConfiguration - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - } - - /// - /// Class UpdateConfiguration - /// - [Route("/System/Configuration", "POST", Summary = "Updates application configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateConfiguration : ServerConfiguration, IReturnVoid - { - } - - [Route("/System/Configuration/{Key}", "POST", Summary = "Updates named configuration")] - [Authenticated(Roles = "Admin")] - public class UpdateNamedConfiguration : IReturnVoid, IRequiresRequestStream - { - [ApiMember(Name = "Key", Description = "Key", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] - public string Key { get; set; } - - public Stream RequestStream { get; set; } - } - - [Route("/System/Configuration/MetadataOptions/Default", "GET", Summary = "Gets a default MetadataOptions object")] - [Authenticated(Roles = "Admin")] - public class GetDefaultMetadataOptions : IReturn - { - - } - - [Route("/System/MediaEncoder/Path", "POST", Summary = "Updates the path to the media encoder")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] - public class UpdateMediaEncoderPath : IReturnVoid - { - [ApiMember(Name = "Path", Description = "Path", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string Path { get; set; } - [ApiMember(Name = "PathType", Description = "PathType", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "POST")] - public string PathType { get; set; } - } - - public class ConfigurationService : BaseApiService - { - /// - /// The _json serializer - /// - private readonly IJsonSerializer _jsonSerializer; - - /// - /// The _configuration manager - /// - private readonly IServerConfigurationManager _configurationManager; - - private readonly IMediaEncoder _mediaEncoder; - - public ConfigurationService( - ILogger logger, - IServerConfigurationManager serverConfigurationManager, - IHttpResultFactory httpResultFactory, - IJsonSerializer jsonSerializer, - IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder) - : base(logger, serverConfigurationManager, httpResultFactory) - { - _jsonSerializer = jsonSerializer; - _configurationManager = configurationManager; - _mediaEncoder = mediaEncoder; - } - - public void Post(UpdateMediaEncoderPath request) - { - _mediaEncoder.UpdateEncoderPath(request.Path, request.PathType); - } - - /// - /// Gets the specified request. - /// - /// The request. - /// System.Object. - public object Get(GetConfiguration request) - { - return ToOptimizedResult(_configurationManager.Configuration); - } - - public object Get(GetNamedConfiguration request) - { - var result = _configurationManager.GetConfiguration(request.Key); - - return ToOptimizedResult(result); - } - - /// - /// Posts the specified configuraiton. - /// - /// The request. - public void Post(UpdateConfiguration request) - { - // Silly, but we need to serialize and deserialize or the XmlSerializer will write the xml with an element name of UpdateConfiguration - var json = _jsonSerializer.SerializeToString(request); - - var config = _jsonSerializer.DeserializeFromString(json); - - _configurationManager.ReplaceConfiguration(config); - } - - public async Task Post(UpdateNamedConfiguration request) - { - var key = GetPathValue(2).ToString(); - - var configurationType = _configurationManager.GetConfigurationType(key); - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(request.RequestStream, configurationType).ConfigureAwait(false); - - _configurationManager.SaveConfiguration(key, configuration); - } - - public object Get(GetDefaultMetadataOptions request) - { - return ToOptimizedResult(new MetadataOptions()); - } - } -} From c6eebca335d09d6a6c627205e126448ab5441f37 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:29:28 -0600 Subject: [PATCH 67/97] Apply suggestions and add URL to log message --- .../Middleware/ExceptionMiddleware.cs | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 0d9dac89f0..ecc76594e4 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Net.Mime; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; @@ -22,15 +23,15 @@ namespace Jellyfin.Server.Middleware /// Initializes a new instance of the class. /// /// Next request delegate. - /// Instance of the interface. + /// Instance of the interface. /// Instance of the interface. public ExceptionMiddleware( RequestDelegate next, - ILoggerFactory loggerFactory, + ILogger logger, IServerConfigurationManager serverConfigurationManager) { _next = next; - _logger = loggerFactory.CreateLogger(); + _logger = logger; _configuration = serverConfigurationManager; } @@ -54,9 +55,14 @@ namespace Jellyfin.Server.Middleware } ex = GetActualException(ex); - _logger.LogError(ex, "Error processing request: {0}", ex.Message); + _logger.LogError( + ex, + "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ", + ex.Message, + context.Request.Method, + context.Request.Path); context.Response.StatusCode = GetStatusCode(ex); - context.Response.ContentType = "text/plain"; + context.Response.ContentType = MediaTypeNames.Text.Plain; var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); @@ -105,16 +111,14 @@ namespace Jellyfin.Server.Middleware } // Strip any information we don't want to reveal - msg = msg.Replace( - _configuration.ApplicationPaths.ProgramSystemPath, - string.Empty, - StringComparison.OrdinalIgnoreCase); - msg = msg.Replace( - _configuration.ApplicationPaths.ProgramDataPath, - string.Empty, - StringComparison.OrdinalIgnoreCase); - - return msg; + return msg.Replace( + _configuration.ApplicationPaths.ProgramSystemPath, + string.Empty, + StringComparison.OrdinalIgnoreCase) + .Replace( + _configuration.ApplicationPaths.ProgramDataPath, + string.Empty, + StringComparison.OrdinalIgnoreCase); } } } From c7c2f9da90a7cf7a452de0ab1adf7e36f422bbe1 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:51:04 -0600 Subject: [PATCH 68/97] Apply suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 2db7e32aad..14c59593fb 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -59,7 +59,7 @@ namespace Jellyfin.Api.Controllers PreferredMetadataLanguage = _config.Configuration.PreferredMetadataLanguage }; - return Ok(result); + return result; } /// @@ -108,7 +108,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetFirstUser() { var user = _userManager.Users.First(); - return Ok(new StartupUserDto { Name = user.Name, Password = user.Password }); + return new StartupUserDto { Name = user.Name, Password = user.Password }; } /// From 4d894c4344fd23026bbfdc0a1cdd24231441a444 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 07:55:47 -0600 Subject: [PATCH 69/97] Remove unneeded Ok calls. --- Jellyfin.Api/Controllers/DevicesController.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 559a260071..cebb51ccfe 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(deviceInfo); + return deviceInfo; } /// @@ -86,7 +86,7 @@ namespace Jellyfin.Api.Controllers [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) + public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) { var deviceInfo = _deviceManager.GetDeviceOptions(id); if (deviceInfo == null) @@ -94,7 +94,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - return Ok(deviceInfo); + return deviceInfo; } /// @@ -150,7 +150,7 @@ namespace Jellyfin.Api.Controllers public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return Ok(uploadHistory); + return uploadHistory; } /// From bb8e738a0817be2e13a8b21929d0f0aeb0c6a461 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 10:03:54 -0600 Subject: [PATCH 70/97] Fix Authorize attributes --- .../Controllers/ConfigurationController.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 14e45833f0..b508ac0547 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,13 @@ #nullable enable using System.Threading.Tasks; +using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Controller.Net; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Serialization; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -17,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// Configuration Controller. /// [Route("System")] - [Authenticated] + [Authorize] public class ConfigurationController : BaseJellyfinApiController { private readonly IServerConfigurationManager _configurationManager; @@ -48,7 +49,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetConfiguration() { - return Ok(_configurationManager.Configuration); + return _configurationManager.Configuration; } /// @@ -57,7 +58,7 @@ namespace Jellyfin.Api.Controllers /// Configuration. /// Status. [HttpPost("Configuration")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration) { @@ -74,7 +75,7 @@ namespace Jellyfin.Api.Controllers [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNamedConfiguration([FromRoute] string key) { - return Ok(_configurationManager.GetConfiguration(key)); + return _configurationManager.GetConfiguration(key); } /// @@ -83,7 +84,7 @@ namespace Jellyfin.Api.Controllers /// Configuration key. /// Status. [HttpPost("Configuration/{Key}")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateNamedConfiguration([FromRoute] string key) { @@ -104,11 +105,11 @@ namespace Jellyfin.Api.Controllers /// /// MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetDefaultMetadataOptions() { - return Ok(new MetadataOptions()); + return new MetadataOptions(); } /// @@ -117,7 +118,7 @@ namespace Jellyfin.Api.Controllers /// Media encoder path form body. /// Status. [HttpPost("MediaEncoder/Path")] - [Authenticated(Roles = "Admin", AllowBeforeStartupWizard = true)] + [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath) { From 3c34d956088430da08bdd812c05d6a87c3bf9d25 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:23:29 -0600 Subject: [PATCH 71/97] Address comments --- .../Middleware/ExceptionMiddleware.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index ecc76594e4..6ebe015030 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -1,8 +1,10 @@ using System; using System.IO; using System.Net.Mime; +using System.Net.Sockets; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; using Microsoft.AspNetCore.Http; @@ -55,15 +57,35 @@ namespace Jellyfin.Server.Middleware } ex = GetActualException(ex); - _logger.LogError( - ex, - "Error processing request: {ExceptionMessage}. URL {Method} {Url}. ", - ex.Message, - context.Request.Method, - context.Request.Path); + + bool ignoreStackTrace = + ex is SocketException + || ex is IOException + || ex is OperationCanceledException + || ex is SecurityException + || ex is AuthenticationException + || ex is FileNotFoundException; + + if (ignoreStackTrace) + { + _logger.LogError( + "Error processing request: {ExceptionMessage}. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + else + { + _logger.LogError( + ex, + "Error processing request. URL {Method} {Url}.", + ex.Message.TrimEnd('.'), + context.Request.Method, + context.Request.Path); + } + context.Response.StatusCode = GetStatusCode(ex); context.Response.ContentType = MediaTypeNames.Text.Plain; - var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } From be50fae38a27878cb520e20ed7956a72d7b05a84 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:24:40 -0600 Subject: [PATCH 72/97] Address comments --- .../Extensions/ApiApplicationBuilderExtensions.cs | 10 ---------- Jellyfin.Server/Startup.cs | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 6c105ab65b..0bd654c7dc 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -24,15 +24,5 @@ namespace Jellyfin.Server.Extensions c.SwaggerEndpoint("/swagger/v1/swagger.json", "Jellyfin API V1"); }); } - - /// - /// Adds exception middleware to the application pipeline. - /// - /// The application builder. - /// The updated application builder. - public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder applicationBuilder) - { - return applicationBuilder.UseMiddleware(); - } } } diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index 7a632f6c44..b17357fc3d 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,4 +1,5 @@ using Jellyfin.Server.Extensions; +using Jellyfin.Server.Middleware; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -58,7 +59,7 @@ namespace Jellyfin.Server app.UseDeveloperExceptionPage(); } - app.UseExceptionMiddleware(); + app.UseMiddleware(); app.UseWebSockets(); From b8508a57d8320085c01a7e2d4656b233169584f2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:38:40 -0600 Subject: [PATCH 73/97] oop --- Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 6ebe015030..0d79bbfaff 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -79,7 +79,6 @@ namespace Jellyfin.Server.Middleware _logger.LogError( ex, "Error processing request. URL {Method} {Url}.", - ex.Message.TrimEnd('.'), context.Request.Method, context.Request.Path); } From 0765ef8bda4d23e33fde7a1bfe49b5a365c6d28e Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 23 Apr 2020 21:41:10 -0600 Subject: [PATCH 74/97] Apply suggestions, fix warning --- Jellyfin.Server/Models/JsonOptions.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs index fa503bc9a4..2f0df3d2c7 100644 --- a/Jellyfin.Server/Models/JsonOptions.cs +++ b/Jellyfin.Server/Models/JsonOptions.cs @@ -7,11 +7,6 @@ namespace Jellyfin.Server.Models /// public static class JsonOptions { - /// - /// Base Json Serializer Options. - /// - private static readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions(); - /// /// Gets CamelCase json options. /// @@ -19,7 +14,7 @@ namespace Jellyfin.Server.Models { get { - var options = _jsonOptions; + var options = DefaultJsonOptions; options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; return options; } @@ -32,10 +27,15 @@ namespace Jellyfin.Server.Models { get { - var options = _jsonOptions; + var options = DefaultJsonOptions; options.PropertyNamingPolicy = null; return options; } } + + /// + /// Gets base Json Serializer Options. + /// + private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); } } From 85853f9ce3d77469b84e3334d7080cd025474ee8 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Fri, 24 Apr 2020 17:11:11 -0600 Subject: [PATCH 75/97] Add back in return type documentation --- Jellyfin.Api/Controllers/NotificationsController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 0bf3aa1b47..8da2a6c536 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -95,6 +95,7 @@ namespace Jellyfin.Api.Controllers /// The description of the notification. /// The URL of the notification. /// The level of the notification. + /// Status. [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CreateAdminNotification( @@ -123,6 +124,7 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. + /// Status. [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRead( @@ -137,6 +139,7 @@ namespace Jellyfin.Api.Controllers /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. + /// Status. [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetUnread( From 890e659cd390fc45c68b42c1a20f24a33e8c1570 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 25 Apr 2020 15:12:18 -0600 Subject: [PATCH 76/97] Fix autolaunch & redirect of swagger. --- Emby.Server.Implementations/Browser/BrowserLauncher.cs | 4 +++- Jellyfin.Server/Program.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 96096e142a..384cb049fa 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,5 +1,7 @@ using System; using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Browser @@ -24,7 +26,7 @@ namespace Emby.Server.Implementations.Browser /// The app host. public static void OpenSwaggerPage(IServerApplicationHost appHost) { - TryOpenUrl(appHost, "/swagger/index.html"); + TryOpenUrl(appHost, "/api-docs/v1/swagger"); } /// diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index e55b0d4ed9..23ddcf159b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -529,7 +529,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "swagger/index.html"; + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger"; } return config From 068368df6352cfad4e69df599c364b3f05b367ba Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 26 Apr 2020 23:28:32 -0600 Subject: [PATCH 77/97] Order actions by route, then http method --- Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 92bacb4400..00a73ade6f 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -105,6 +105,10 @@ namespace Jellyfin.Server.Extensions { c.IncludeXmlComments(xmlFile); } + + // Order actions by route path, then by http method. + c.OrderActionsBy(description => + $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}"); }); } } From c61a200c9de2714b3d6353f3a4ae52b8962d369a Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Tue, 28 Apr 2020 09:30:59 -0600 Subject: [PATCH 78/97] Revise documentation based on discussion in #2872 --- .../Controllers/NotificationsController.cs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 8da2a6c536..8feea9ab61 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -35,13 +35,14 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting a user's notifications. + /// Gets a user's notifications. /// /// The user's ID. /// An optional filter by notification read state. /// The optional index to start at. All notifications with a lower index will be omitted from the results. /// An optional limit on the number of notifications returned. - /// A read-only list of all of the user's notifications. + /// Notifications returned. + /// An containing a list of notifications. [HttpGet("{UserID}")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNotifications( @@ -54,10 +55,11 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting a user's notification summary. + /// Gets a user's notification summary. /// /// The user's ID. - /// Notifications summary for the user. + /// Summary of user's notifications returned. + /// An containing a summary of the users notifications. [HttpGet("{UserID}/Summary")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetNotificationsSummary( @@ -67,9 +69,10 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting notification types. + /// Gets notification types. /// - /// All notification types. + /// All notification types returned. + /// An containing a list of all notification types. [HttpGet("Types")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNotificationTypes() @@ -78,9 +81,10 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting notification services. + /// Gets notification services. /// - /// All notification services. + /// All notification services returned. + /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetNotificationServices() @@ -89,13 +93,14 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to send a notification to all admins. + /// Sends a notification to all admins. /// /// The name of the notification. /// The description of the notification. /// The URL of the notification. /// The level of the notification. - /// Status. + /// Notification sent. + /// An . [HttpPost("Admin")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CreateAdminNotification( @@ -120,11 +125,12 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to set notifications as read. + /// Sets notifications as read. /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as read. - /// Status. + /// Notifications set as read. + /// An . [HttpPost("{UserID}/Read")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRead( @@ -135,11 +141,12 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint to set notifications as unread. + /// Sets notifications as unread. /// /// The userID. /// A comma-separated list of the IDs of notifications which should be set as unread. - /// Status. + /// Notifications set as unread. + /// An . [HttpPost("{UserID}/Unread")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetUnread( From 806ae1bc07e715c6109a3e8ec96c6d3dd6a802ef Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 08:04:05 -0600 Subject: [PATCH 79/97] Remove versioned API --- .../Browser/BrowserLauncher.cs | 2 +- .../ApiApplicationBuilderExtensions.cs | 16 ++++++++-------- .../Extensions/ApiServiceCollectionExtensions.cs | 2 +- Jellyfin.Server/Program.cs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Emby.Server.Implementations/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs index 384cb049fa..e706401fd1 100644 --- a/Emby.Server.Implementations/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -26,7 +26,7 @@ namespace Emby.Server.Implementations.Browser /// The app host. public static void OpenSwaggerPage(IServerApplicationHost appHost) { - TryOpenUrl(appHost, "/api-docs/v1/swagger"); + TryOpenUrl(appHost, "/api-docs/swagger"); } /// diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 33fd77d9c7..745567703f 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,5 +1,4 @@ using MediaBrowser.Controller.Configuration; -using Jellyfin.Server.Middleware; using Microsoft.AspNetCore.Builder; namespace Jellyfin.Server.Extensions @@ -31,19 +30,20 @@ namespace Jellyfin.Server.Extensions return applicationBuilder .UseSwagger(c => { - c.RouteTemplate = $"/{baseUrl}api-docs/{{documentName}}/openapi.json"; + // Custom path requires {documentName}, SwaggerDoc documentName is 'api-docs' + c.RouteTemplate = $"/{baseUrl}{{documentName}}/openapi.json"; }) .UseSwaggerUI(c => { - c.DocumentTitle = "Jellyfin API v1"; - c.SwaggerEndpoint($"/{baseUrl}api-docs/v1/openapi.json", "Jellyfin API v1"); - c.RoutePrefix = $"{baseUrl}api-docs/v1/swagger"; + c.DocumentTitle = "Jellyfin API"; + c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API"); + c.RoutePrefix = $"{baseUrl}api-docs/swagger"; }) .UseReDoc(c => { - c.DocumentTitle = "Jellyfin API v1"; - c.SpecUrl($"/{baseUrl}api-docs/v1/openapi.json"); - c.RoutePrefix = $"{baseUrl}api-docs/v1/redoc"; + c.DocumentTitle = "Jellyfin API"; + c.SpecUrl($"/{baseUrl}api-docs/openapi.json"); + c.RoutePrefix = $"{baseUrl}api-docs/redoc"; }); } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a24785d57e..a354f45aad 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -96,7 +96,7 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); + c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 23ddcf159b..7135800802 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -529,7 +529,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/v1/swagger"; + inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; } return config From 97ecffceb7fe655010c1f415fd688b3ee0f9d48d Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 08:59:34 -0600 Subject: [PATCH 80/97] Add response code descriptions --- Jellyfin.Api/Controllers/StartupController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 14c59593fb..d60e46a01b 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -33,6 +33,7 @@ namespace Jellyfin.Api.Controllers /// /// Api endpoint for completing the startup wizard. /// + /// Startup wizard completed. /// Status. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -47,6 +48,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for getting the initial startup wizard configuration. /// + /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -68,6 +70,7 @@ namespace Jellyfin.Api.Controllers /// The UI language culture. /// The metadata country code. /// The preferred language for metadata. + /// Configuration saved. /// Status. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -88,6 +91,7 @@ namespace Jellyfin.Api.Controllers /// /// Enable remote access. /// Enable UPnP. + /// Configuration saved. /// Status. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -102,6 +106,7 @@ namespace Jellyfin.Api.Controllers /// /// Endpoint for returning the first user. /// + /// Initial user retrieved. /// The first user. [HttpGet("User")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -115,6 +120,7 @@ namespace Jellyfin.Api.Controllers /// Endpoint for updating the user name and password. /// /// The DTO containing username and password. + /// Updated user name and password. /// The async task. [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] From 7a3925b863a12bea492a93f41cda4eb92dc9c183 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 29 Apr 2020 09:41:12 -0600 Subject: [PATCH 81/97] Fix docs --- Jellyfin.Api/Controllers/StartupController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index d60e46a01b..66e4774aa0 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -31,7 +31,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Api endpoint for completing the startup wizard. + /// Completes the startup wizard. /// /// Startup wizard completed. /// Status. @@ -46,7 +46,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for getting the initial startup wizard configuration. + /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. /// The initial startup wizard configuration. @@ -65,7 +65,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the initial startup wizard configuration. + /// Sets the initial startup wizard configuration. /// /// The UI language culture. /// The metadata country code. @@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for (dis)allowing remote access and UPnP. + /// Sets remote access and UPnP. /// /// Enable remote access. /// Enable UPnP. @@ -104,7 +104,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for returning the first user. + /// Gets the first user. /// /// Initial user retrieved. /// The first user. @@ -117,7 +117,7 @@ namespace Jellyfin.Api.Controllers } /// - /// Endpoint for updating the user name and password. + /// Sets the user name and password. /// /// The DTO containing username and password. /// Updated user name and password. From 82231b4393bb367f7fca50fed21f00e469b9f960 Mon Sep 17 00:00:00 2001 From: ZadenRB Date: Wed, 29 Apr 2020 15:53:29 -0600 Subject: [PATCH 82/97] Update to return IEnumerable directly where possible --- Jellyfin.Api/Controllers/NotificationsController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 8feea9ab61..3cbb3a3a3f 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers /// An containing a list of all notification types. [HttpGet("Types")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNotificationTypes() + public IEnumerable GetNotificationTypes() { return _notificationManager.GetNotificationTypes(); } @@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetNotificationServices() + public IEnumerable GetNotificationServices() { - return _notificationManager.GetNotificationServices().ToList(); + return _notificationManager.GetNotificationServices(); } /// From 25002483a3fd7f9d1c79c74338ac18c8eabfb0ed Mon Sep 17 00:00:00 2001 From: crobibero Date: Sat, 2 May 2020 17:23:02 -0600 Subject: [PATCH 83/97] Update endpoint docs --- Jellyfin.Api/Controllers/DevicesController.cs | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index cebb51ccfe..02cf1bc446 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -46,11 +47,12 @@ namespace Jellyfin.Api.Controllers /// /// /// Gets or sets a value indicating whether [supports synchronize]. /// /// Gets or sets the user identifier. - /// Device Infos. + /// Devices retrieved. + /// An containing the list of devices. [HttpGet] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; var devices = _deviceManager.GetDevices(deviceQuery); @@ -61,7 +63,9 @@ namespace Jellyfin.Api.Controllers /// Get info for a device. /// /// Device Id. - /// Device Info. + /// Device info retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. [HttpGet("Info")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -81,7 +85,9 @@ namespace Jellyfin.Api.Controllers /// Get options for a device. /// /// Device Id. - /// Device Info. + /// Device options retrieved. + /// Device not found. + /// An containing the device info on success, or a if the device could not be found. [HttpGet("Options")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -102,7 +108,9 @@ namespace Jellyfin.Api.Controllers /// /// Device Id. /// Device Options. - /// Status. + /// Device options updated. + /// Device not found. + /// An on success, or a if the device could not be found. [HttpPost("Options")] [Authenticated(Roles = "Admin")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -125,11 +133,19 @@ namespace Jellyfin.Api.Controllers /// Deletes a device. /// /// Device Id. - /// Status. + /// Device deleted. + /// Device not found. + /// An on success, or a if the device could not be found. [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult DeleteDevice([FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + var sessions = _authenticationRepository.Get(new AuthenticationInfoQuery { DeviceId = id }).Items; foreach (var session in sessions) @@ -144,11 +160,19 @@ namespace Jellyfin.Api.Controllers /// Gets camera upload history for a device. /// /// Device Id. - /// Content Upload History. + /// Device upload history retrieved. + /// Device not found. + /// An containing the device upload history on success, or a if the device could not be found. [HttpGet("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + var uploadHistory = _deviceManager.GetCameraUploadHistory(id); return uploadHistory; } @@ -160,7 +184,14 @@ namespace Jellyfin.Api.Controllers /// Album. /// Name. /// Id. - /// Status. + /// Contents uploaded. + /// No uploaded contents. + /// Device not found. + /// + /// An on success, + /// or a if the device could not be found + /// or a if the upload contains no files. + /// [HttpPost("CameraUploads")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -170,6 +201,12 @@ namespace Jellyfin.Api.Controllers [FromQuery, BindRequired] string name, [FromQuery, BindRequired] string id) { + var existingDevice = _deviceManager.GetDevice(id); + if (existingDevice == null) + { + return NotFound(); + } + Stream fileStream; string contentType; From d7d8118b42c8abc8a4f12c4f2b0fb97cc6384ba7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 3 May 2020 14:02:15 -0600 Subject: [PATCH 84/97] Fix xml docs --- Jellyfin.Api/Controllers/NotificationsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/NotificationsController.cs b/Jellyfin.Api/Controllers/NotificationsController.cs index 3cbb3a3a3f..8d82ca10f1 100644 --- a/Jellyfin.Api/Controllers/NotificationsController.cs +++ b/Jellyfin.Api/Controllers/NotificationsController.cs @@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets notification services. /// - /// All notification services returned. + /// All notification services returned. /// An containing a list of all notification services. [HttpGet("Services")] [ProducesResponseType(StatusCodes.Status200OK)] From 2b1b9a64b6b7a3bac4d96642cda7a0c55d5cae74 Mon Sep 17 00:00:00 2001 From: crobibero Date: Fri, 8 May 2020 08:40:37 -0600 Subject: [PATCH 85/97] Add OperationId to SwaggerGen --- .../Extensions/ApiServiceCollectionExtensions.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index a354f45aad..344ef6a5ff 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; @@ -14,6 +15,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Jellyfin.Server.Extensions { @@ -112,6 +114,10 @@ namespace Jellyfin.Server.Extensions // Order actions by route path, then by http method. c.OrderActionsBy(description => $"{description.ActionDescriptor.RouteValues["controller"]}_{description.HttpMethod}"); + + // Use method name as operationId + c.CustomOperationIds(description => + description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); }); } } From 37f55b5c217db5343ab196094f67dc84e71d4ef0 Mon Sep 17 00:00:00 2001 From: crobibero Date: Sun, 17 May 2020 19:56:02 -0600 Subject: [PATCH 86/97] apply doc suggestions --- Jellyfin.Api/Controllers/StartupController.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/StartupController.cs b/Jellyfin.Api/Controllers/StartupController.cs index 66e4774aa0..ed1dc1ede3 100644 --- a/Jellyfin.Api/Controllers/StartupController.cs +++ b/Jellyfin.Api/Controllers/StartupController.cs @@ -34,7 +34,7 @@ namespace Jellyfin.Api.Controllers /// Completes the startup wizard. /// /// Startup wizard completed. - /// Status. + /// An indicating success. [HttpPost("Complete")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult CompleteWizard() @@ -49,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Gets the initial startup wizard configuration. /// /// Initial startup wizard configuration retrieved. - /// The initial startup wizard configuration. + /// An containing the initial startup wizard configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetStartupConfiguration() @@ -71,7 +71,7 @@ namespace Jellyfin.Api.Controllers /// The metadata country code. /// The preferred language for metadata. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult UpdateInitialConfiguration( @@ -92,7 +92,7 @@ namespace Jellyfin.Api.Controllers /// Enable remote access. /// Enable UPnP. /// Configuration saved. - /// Status. + /// An indicating success. [HttpPost("RemoteAccess")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping) @@ -121,7 +121,10 @@ namespace Jellyfin.Api.Controllers /// /// The DTO containing username and password. /// Updated user name and password. - /// The async task. + /// + /// A that represents the asynchronous update operation. + /// The task result contains an indicating success. + /// [HttpPost("User")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UpdateUser([FromForm] StartupUserDto startupUserDto) From 6dbbfcbfbef28e8866aa0144170e1edfff1a2bcb Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 09:29:59 -0600 Subject: [PATCH 87/97] update xml docs --- Jellyfin.Api/Controllers/ConfigurationController.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index b508ac0547..992cb00874 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -44,6 +44,7 @@ namespace Jellyfin.Api.Controllers /// /// Gets application configuration. /// + /// Application configuration returned. /// Application configuration. [HttpGet("Configuration")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -56,7 +57,8 @@ namespace Jellyfin.Api.Controllers /// Updates application configuration. /// /// Configuration. - /// Status. + /// Configuration updated. + /// Update status. [HttpPost("Configuration")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -70,6 +72,7 @@ namespace Jellyfin.Api.Controllers /// Gets a named configuration. /// /// Configuration key. + /// Configuration returned. /// Configuration. [HttpGet("Configuration/{Key}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -82,7 +85,8 @@ namespace Jellyfin.Api.Controllers /// Updates named configuration. /// /// Configuration key. - /// Status. + /// Named configuration updated. + /// Update status. [HttpPost("Configuration/{Key}")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -103,7 +107,8 @@ namespace Jellyfin.Api.Controllers /// /// Gets a default MetadataOptions object. /// - /// MetadataOptions. + /// Metadata options returned. + /// Default MetadataOptions. [HttpGet("Configuration/MetadataOptions/Default")] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] @@ -116,6 +121,7 @@ namespace Jellyfin.Api.Controllers /// Updates the path to the media encoder. /// /// Media encoder path form body. + /// Media encoder path updated. /// Status. [HttpPost("MediaEncoder/Path")] [Authorize(Policy = Policies.FirstTimeSetupOrElevated)] From a11a1934399b8cbce0487ced49d2f8e7065b436a Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:04:09 -0600 Subject: [PATCH 88/97] Remove CameraUpload endpoints --- Jellyfin.Api/Controllers/DevicesController.cs | 83 ------------------- 1 file changed, 83 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 02cf1bc446..64dc2322dd 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,9 +2,6 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; @@ -155,85 +152,5 @@ namespace Jellyfin.Api.Controllers return Ok(); } - - /// - /// Gets camera upload history for a device. - /// - /// Device Id. - /// Device upload history retrieved. - /// Device not found. - /// An containing the device upload history on success, or a if the device could not be found. - [HttpGet("CameraUploads")] - [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult GetCameraUploads([FromQuery, BindRequired] string id) - { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice == null) - { - return NotFound(); - } - - var uploadHistory = _deviceManager.GetCameraUploadHistory(id); - return uploadHistory; - } - - /// - /// Uploads content. - /// - /// Device Id. - /// Album. - /// Name. - /// Id. - /// Contents uploaded. - /// No uploaded contents. - /// Device not found. - /// - /// An on success, - /// or a if the device could not be found - /// or a if the upload contains no files. - /// - [HttpPost("CameraUploads")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task PostCameraUploadAsync( - [FromQuery, BindRequired] string deviceId, - [FromQuery, BindRequired] string album, - [FromQuery, BindRequired] string name, - [FromQuery, BindRequired] string id) - { - var existingDevice = _deviceManager.GetDevice(id); - if (existingDevice == null) - { - return NotFound(); - } - - Stream fileStream; - string contentType; - - if (Request.HasFormContentType) - { - if (Request.Form.Files.Any()) - { - fileStream = Request.Form.Files[0].OpenReadStream(); - contentType = Request.Form.Files[0].ContentType; - } - else - { - return BadRequest(); - } - } - else - { - fileStream = Request.Body; - contentType = Request.ContentType; - } - - await _deviceManager.AcceptCameraUpload( - deviceId, - fileStream, - new LocalFileInfo { MimeType = contentType, Album = album, Name = name, Id = id }).ConfigureAwait(false); - - return Ok(); - } } } From cf78edc979b626ff11ff88889f618cba50c5ee5f Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:05:23 -0600 Subject: [PATCH 89/97] Fix Authorize attributes --- Jellyfin.Api/Controllers/DevicesController.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index 64dc2322dd..b22b5f985b 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -2,11 +2,13 @@ using System; using System.Collections.Generic; +using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -16,7 +18,7 @@ namespace Jellyfin.Api.Controllers /// /// Devices Controller. /// - [Authenticated] + [Authorize] public class DevicesController : BaseJellyfinApiController { private readonly IDeviceManager _deviceManager; @@ -47,7 +49,7 @@ namespace Jellyfin.Api.Controllers /// Devices retrieved. /// An containing the list of devices. [HttpGet] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { @@ -64,7 +66,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An containing the device info on success, or a if the device could not be found. [HttpGet("Info")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDeviceInfo([FromQuery, BindRequired] string id) @@ -86,7 +88,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An containing the device info on success, or a if the device could not be found. [HttpGet("Options")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetDeviceOptions([FromQuery, BindRequired] string id) @@ -109,7 +111,7 @@ namespace Jellyfin.Api.Controllers /// Device not found. /// An on success, or a if the device could not be found. [HttpPost("Options")] - [Authenticated(Roles = "Admin")] + [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult UpdateDeviceOptions( From cdb25e355c6ebf9ba09f44a7bb7e35286e50976e Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 19 May 2020 10:06:25 -0600 Subject: [PATCH 90/97] Fix return value --- Jellyfin.Api/Controllers/DevicesController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index b22b5f985b..a46d3f9370 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; +using MediaBrowser.Model.Querying; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -51,11 +52,10 @@ namespace Jellyfin.Api.Controllers [HttpGet] [Authorize(Policy = Policies.RequiresElevation)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public ActionResult> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) { var deviceQuery = new DeviceQuery { SupportsSync = supportsSync, UserId = userId ?? Guid.Empty }; - var devices = _deviceManager.GetDevices(deviceQuery); - return Ok(devices); + return _deviceManager.GetDevices(deviceQuery); } /// From a5a39300bc733ad7b1d3c683f5f290a742171661 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 26 May 2020 16:55:27 +0200 Subject: [PATCH 91/97] Don't send Exception message in Production Environment --- .../Middleware/ExceptionMiddleware.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index 0d79bbfaff..dd4d1ee99b 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -7,7 +7,9 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Middleware @@ -20,6 +22,7 @@ namespace Jellyfin.Server.Middleware private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly IServerConfigurationManager _configuration; + private readonly IWebHostEnvironment _hostEnvironment; /// /// Initializes a new instance of the class. @@ -27,14 +30,17 @@ namespace Jellyfin.Server.Middleware /// Next request delegate. /// Instance of the interface. /// Instance of the interface. + /// Instance of the interface. public ExceptionMiddleware( RequestDelegate next, ILogger logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IWebHostEnvironment hostEnvironment) { _next = next; _logger = logger; _configuration = serverConfigurationManager; + _hostEnvironment = hostEnvironment; } /// @@ -85,6 +91,14 @@ namespace Jellyfin.Server.Middleware context.Response.StatusCode = GetStatusCode(ex); context.Response.ContentType = MediaTypeNames.Text.Plain; + + // Don't send exception unless the server is in a Development environment + if (!_hostEnvironment.IsDevelopment()) + { + await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false); + return; + } + var errorContent = NormalizeExceptionMessage(ex.Message); await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } From 6d9f564a949e326e909cbcfd37d254195b40ba56 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 2 Jun 2020 15:04:55 +0200 Subject: [PATCH 92/97] Remove duplicate code Co-authored-by: Vasily --- Jellyfin.Server/Middleware/ExceptionMiddleware.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs index dd4d1ee99b..63effafc19 100644 --- a/Jellyfin.Server/Middleware/ExceptionMiddleware.cs +++ b/Jellyfin.Server/Middleware/ExceptionMiddleware.cs @@ -93,13 +93,9 @@ namespace Jellyfin.Server.Middleware context.Response.ContentType = MediaTypeNames.Text.Plain; // Don't send exception unless the server is in a Development environment - if (!_hostEnvironment.IsDevelopment()) - { - await context.Response.WriteAsync("Error processing request.").ConfigureAwait(false); - return; - } - - var errorContent = NormalizeExceptionMessage(ex.Message); + var errorContent = _hostEnvironment.IsDevelopment() + ? NormalizeExceptionMessage(ex.Message) + : "Error processing request."; await context.Response.WriteAsync(errorContent).ConfigureAwait(false); } } From 4fe0beec162d4554f1d6cc3c658b672eafbfa307 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 21 May 2020 08:44:15 -0600 Subject: [PATCH 93/97] Fix Json Enum conversion, map all JsonDefaults properties to API --- .../ApiServiceCollectionExtensions.cs | 22 +++++++--- .../CamelCaseJsonProfileFormatter.cs | 4 +- .../PascalCaseJsonProfileFormatter.cs | 4 +- Jellyfin.Server/Models/JsonOptions.cs | 41 ------------------- MediaBrowser.Common/Json/JsonDefaults.cs | 34 ++++++++++++++- 5 files changed, 53 insertions(+), 52 deletions(-) delete mode 100644 Jellyfin.Server/Models/JsonOptions.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 344ef6a5ff..b9f55e2008 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; -using System.Text.Json.Serialization; using Jellyfin.Api; using Jellyfin.Api.Auth; using Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy; @@ -11,6 +8,7 @@ using Jellyfin.Api.Auth.RequiresElevationPolicy; using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -83,8 +81,20 @@ namespace Jellyfin.Server.Extensions .AddApplicationPart(typeof(StartupController).Assembly) .AddJsonOptions(options => { - // Setting the naming policy to null leaves the property names as-is when serializing objects to JSON. - options.JsonSerializerOptions.PropertyNamingPolicy = null; + // Update all properties that are set in JsonDefaults + var jsonOptions = JsonDefaults.PascalCase; + + // From JsonDefaults + options.JsonSerializerOptions.ReadCommentHandling = jsonOptions.ReadCommentHandling; + options.JsonSerializerOptions.WriteIndented = jsonOptions.WriteIndented; + options.JsonSerializerOptions.Converters.Clear(); + foreach (var converter in jsonOptions.Converters) + { + options.JsonSerializerOptions.Converters.Add(converter); + } + + // From JsonDefaults.PascalCase + options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; }) .AddControllersAsServices(); } @@ -98,7 +108,7 @@ namespace Jellyfin.Server.Extensions { return serviceCollection.AddSwaggerGen(c => { - c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API" }); + c.SwaggerDoc("api-docs", new OpenApiInfo { Title = "Jellyfin API", Version = "v1" }); // Add all xml doc files to swagger generator. var xmlFiles = Directory.GetFiles( diff --git a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs index e6ad6dfb13..989c8ecea2 100644 --- a/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/CamelCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public CamelCaseJsonProfileFormatter() : base(JsonOptions.CamelCase) + public CamelCaseJsonProfileFormatter() : base(JsonDefaults.CamelCase) { SupportedMediaTypes.Clear(); SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json;profile=\"CamelCase\"")); diff --git a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs index 675f4c79ee..69963b3fb3 100644 --- a/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs +++ b/Jellyfin.Server/Formatters/PascalCaseJsonProfileFormatter.cs @@ -1,4 +1,4 @@ -using Jellyfin.Server.Models; +using MediaBrowser.Common.Json; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -12,7 +12,7 @@ namespace Jellyfin.Server.Formatters /// /// Initializes a new instance of the class. /// - public PascalCaseJsonProfileFormatter() : base(JsonOptions.PascalCase) + public PascalCaseJsonProfileFormatter() : base(JsonDefaults.PascalCase) { SupportedMediaTypes.Clear(); // Add application/json for default formatter diff --git a/Jellyfin.Server/Models/JsonOptions.cs b/Jellyfin.Server/Models/JsonOptions.cs deleted file mode 100644 index 2f0df3d2c7..0000000000 --- a/Jellyfin.Server/Models/JsonOptions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json; - -namespace Jellyfin.Server.Models -{ - /// - /// Json Options. - /// - public static class JsonOptions - { - /// - /// Gets CamelCase json options. - /// - public static JsonSerializerOptions CamelCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - return options; - } - } - - /// - /// Gets PascalCase json options. - /// - public static JsonSerializerOptions PascalCase - { - get - { - var options = DefaultJsonOptions; - options.PropertyNamingPolicy = null; - return options; - } - } - - /// - /// Gets base Json Serializer Options. - /// - private static JsonSerializerOptions DefaultJsonOptions => new JsonSerializerOptions(); - } -} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 4a6ee0a793..326f04eea1 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -12,10 +12,16 @@ namespace MediaBrowser.Common.Json /// /// Gets the default options. /// + /// + /// When changing these options, update + /// Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs + /// -> AddJellyfinApi + /// -> AddJsonOptions + /// /// The default options. public static JsonSerializerOptions GetOptions() { - var options = new JsonSerializerOptions() + var options = new JsonSerializerOptions { ReadCommentHandling = JsonCommentHandling.Disallow, WriteIndented = false @@ -26,5 +32,31 @@ namespace MediaBrowser.Common.Json return options; } + + /// + /// Gets CamelCase json options. + /// + public static JsonSerializerOptions CamelCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + return options; + } + } + + /// + /// Gets PascalCase json options. + /// + public static JsonSerializerOptions PascalCase + { + get + { + var options = GetOptions(); + options.PropertyNamingPolicy = null; + return options; + } + } } } From 0e41c4727d84edbf4d7b96c59e0a3d3bec87b633 Mon Sep 17 00:00:00 2001 From: crobibero Date: Mon, 1 Jun 2020 10:36:32 -0600 Subject: [PATCH 94/97] revert to System.Text.JsonSerializer --- .../Controllers/ConfigurationController.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs index 992cb00874..8243bfce4c 100644 --- a/Jellyfin.Api/Controllers/ConfigurationController.cs +++ b/Jellyfin.Api/Controllers/ConfigurationController.cs @@ -1,12 +1,12 @@ #nullable enable +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Models.ConfigurationDtos; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Serialization; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -23,22 +23,18 @@ namespace Jellyfin.Api.Controllers { private readonly IServerConfigurationManager _configurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly IJsonSerializer _jsonSerializer; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. - /// Instance of the interface. public ConfigurationController( IServerConfigurationManager configurationManager, - IMediaEncoder mediaEncoder, - IJsonSerializer jsonSerializer) + IMediaEncoder mediaEncoder) { _configurationManager = configurationManager; _mediaEncoder = mediaEncoder; - _jsonSerializer = jsonSerializer; } /// @@ -93,13 +89,7 @@ namespace Jellyfin.Api.Controllers public async Task UpdateNamedConfiguration([FromRoute] string key) { var configurationType = _configurationManager.GetConfigurationType(key); - /* - // TODO switch to System.Text.Json when https://github.com/dotnet/runtime/issues/30255 is fixed. var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType); - */ - - var configuration = await _jsonSerializer.DeserializeFromStreamAsync(Request.Body, configurationType) - .ConfigureAwait(false); _configurationManager.SaveConfiguration(key, configuration); return Ok(); } From 01a5103fef83bbbef230faf2303d16648981a5d2 Mon Sep 17 00:00:00 2001 From: crobibero Date: Tue, 2 Jun 2020 11:47:00 -0600 Subject: [PATCH 95/97] Add Dictionary with non-string keys to System.Text.Json --- .../ApiServiceCollectionExtensions.cs | 26 ++++++ .../JsonNonStringKeyDictionaryConverter.cs | 79 +++++++++++++++++++ ...nNonStringKeyDictionaryConverterFactory.cs | 60 ++++++++++++++ MediaBrowser.Common/Json/JsonDefaults.cs | 1 + 4 files changed, 166 insertions(+) create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs create mode 100644 MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index b9f55e2008..cb4189587d 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Jellyfin.Api; using Jellyfin.Api.Auth; @@ -9,6 +11,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; using MediaBrowser.Common.Json; +using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; @@ -128,7 +131,30 @@ namespace Jellyfin.Server.Extensions // Use method name as operationId c.CustomOperationIds(description => description.TryGetMethodInfo(out MethodInfo methodInfo) ? methodInfo.Name : null); + + // TODO - remove when all types are supported in System.Text.Json + c.AddSwaggerTypeMappings(); }); } + + private static void AddSwaggerTypeMappings(this SwaggerGenOptions options) + { + /* + * TODO remove when System.Text.Json supports non-string keys. + * Used in Jellyfin.Api.Controller.GetChannels. + */ + options.MapType>(() => + new OpenApiSchema + { + Type = "object", + Properties = typeof(ImageType).GetEnumNames().ToDictionary( + name => name, + name => new OpenApiSchema + { + Type = "string", + Format = "string" + }) + }); + } } } diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs new file mode 100644 index 0000000000..f2ddd7fea2 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -0,0 +1,79 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// Converter for Dictionaries without string key. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + /// Type of key. + /// Type of value. + internal sealed class JsonNonStringKeyDictionaryConverter : JsonConverter> + { + /// + /// Read JSON. + /// + /// The Utf8JsonReader. + /// The type to convert. + /// The json serializer options. + /// Typed dictionary. + /// + public override IDictionary Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var convertedType = typeof(Dictionary<,>).MakeGenericType(typeof(string), typeToConvert.GenericTypeArguments[1]); + var value = JsonSerializer.Deserialize(ref reader, convertedType, options); + var instance = (Dictionary)Activator.CreateInstance( + typeToConvert, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + var enumerator = (IEnumerator)convertedType.GetMethod("GetEnumerator")!.Invoke(value, null); + var parse = typeof(TKey).GetMethod( + "Parse", + 0, + BindingFlags.Public | BindingFlags.Static, + null, + CallingConventions.Any, + new[] { typeof(string) }, + null); + if (parse == null) + { + throw new NotSupportedException($"{typeof(TKey)} as TKey in IDictionary is not supported."); + } + + while (enumerator.MoveNext()) + { + var element = (KeyValuePair)enumerator.Current; + instance.Add((TKey)parse.Invoke(null, new[] { (object?) element.Key }), element.Value); + } + + return instance; + } + + /// + /// Write dictionary as Json. + /// + /// The Utf8JsonWriter. + /// The dictionary value. + /// The Json serializer options. + public override void Write(Utf8JsonWriter writer, IDictionary value, JsonSerializerOptions options) + { + var convertedDictionary = new Dictionary(value.Count); + foreach (var (k, v) in value) + { + convertedDictionary[k?.ToString()] = v; + } + JsonSerializer.Serialize(writer, convertedDictionary, options); + convertedDictionary.Clear(); + } + } +} diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs new file mode 100644 index 0000000000..d9795a189a --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverterFactory.cs @@ -0,0 +1,60 @@ +#nullable enable + +using System; +using System.Collections; +using System.Globalization; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// + /// https://github.com/dotnet/runtime/issues/30524#issuecomment-524619972. + /// TODO This can be removed when System.Text.Json supports Dictionaries with non-string keys. + /// + internal sealed class JsonNonStringKeyDictionaryConverterFactory : JsonConverterFactory + { + /// + /// Only convert objects that implement IDictionary and do not have string keys. + /// + /// Type convert. + /// Conversion ability. + public override bool CanConvert(Type typeToConvert) + { + + if (!typeToConvert.IsGenericType) + { + return false; + } + + // Let built in converter handle string keys + if (typeToConvert.GenericTypeArguments[0] == typeof(string)) + { + return false; + } + + // Only support objects that implement IDictionary + return typeToConvert.GetInterface(nameof(IDictionary)) != null; + } + + /// + /// Create converter for generic dictionary type. + /// + /// Type to convert. + /// Json serializer options. + /// JsonConverter for given type. + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var converterType = typeof(JsonNonStringKeyDictionaryConverter<,>) + .MakeGenericType(typeToConvert.GenericTypeArguments[0], typeToConvert.GenericTypeArguments[1]); + var converter = (JsonConverter)Activator.CreateInstance( + converterType, + BindingFlags.Instance | BindingFlags.Public, + null, + null, + CultureInfo.CurrentCulture); + return converter; + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 326f04eea1..f38e2893ec 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -29,6 +29,7 @@ namespace MediaBrowser.Common.Json options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonNonStringKeyDictionaryConverterFactory()); return options; } From 8b59934ccb53fda0ccfda2e914192ac3d1d11ad7 Mon Sep 17 00:00:00 2001 From: crobibero Date: Wed, 3 Jun 2020 09:12:12 -0600 Subject: [PATCH 96/97] remove extra Clear call --- .../Json/Converters/JsonNonStringKeyDictionaryConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs index f2ddd7fea2..636ef5372f 100644 --- a/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs +++ b/MediaBrowser.Common/Json/Converters/JsonNonStringKeyDictionaryConverter.cs @@ -73,7 +73,6 @@ namespace MediaBrowser.Common.Json.Converters convertedDictionary[k?.ToString()] = v; } JsonSerializer.Serialize(writer, convertedDictionary, options); - convertedDictionary.Clear(); } } } From 3e749eabdf10f9a070a6c303ec37a912f9657e58 Mon Sep 17 00:00:00 2001 From: crobibero Date: Thu, 4 Jun 2020 07:29:00 -0600 Subject: [PATCH 97/97] Fix doc errors --- Jellyfin.Api/Controllers/DevicesController.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index a46d3f9370..1e75579033 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -1,10 +1,8 @@ #nullable enable using System; -using System.Collections.Generic; using Jellyfin.Api.Constants; using MediaBrowser.Controller.Devices; -using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Devices; @@ -45,8 +43,8 @@ namespace Jellyfin.Api.Controllers /// /// Get Devices. /// - /// /// Gets or sets a value indicating whether [supports synchronize]. - /// /// Gets or sets the user identifier. + /// Gets or sets a value indicating whether [supports synchronize]. + /// Gets or sets the user identifier. /// Devices retrieved. /// An containing the list of devices. [HttpGet]