diff options
242 files changed, 4665 insertions, 12200 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index b558d2a6f..4d38a906e 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -62,7 +62,6 @@ jobs: - task: DownloadPipelineArtifact@2 displayName: 'Download Reference Assembly Build Artifact' - enabled: false inputs: source: "specific" artifact: "$(NugetPackageName)" @@ -74,7 +73,6 @@ jobs: - task: CopyFiles@2 displayName: 'Copy Reference Assembly Build Artifact' - enabled: false inputs: sourceFolder: $(System.ArtifactsDirectory)/current-artifacts contents: '**/*.dll' @@ -85,7 +83,6 @@ jobs: - task: DotNetCoreCLI@2 displayName: 'Execute ABI Compatibility Check Tool' - enabled: false inputs: command: custom custom: compat diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 4631badae..cc845afd4 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -42,7 +42,7 @@ jobs: - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="no" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)' displayName: 'Run Dockerfile (stable)' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - task: PublishPipelineArtifact@1 displayName: 'Publish Release' @@ -87,7 +87,7 @@ jobs: steps: - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" displayName: Set release version (stable) - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') - task: Docker@2 displayName: 'Push Unstable Image' @@ -104,7 +104,7 @@ jobs: - task: Docker@2 displayName: 'Push Stable Image' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: repository: 'jellyfin/jellyfin-server' command: buildAndPush @@ -116,8 +116,9 @@ jobs: $(JellyfinVersion)-$(BuildConfiguration) - job: CollectArtifacts - timeoutInMinutes: 10 + timeoutInMinutes: 20 displayName: 'Collect Artifacts' + continueOnError: true dependsOn: - BuildPackage - BuildDocker @@ -129,20 +130,22 @@ jobs: steps: - task: SSH@0 displayName: 'Update Unstable Repository' + continueOnError: true condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: sshEndpoint: repository runOptions: 'commands' - commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable + commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) unstable & - task: SSH@0 displayName: 'Update Stable Repository' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + continueOnError: true + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: sshEndpoint: repository runOptions: 'commands' - commands: sudo -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) - + commands: sudo nohup -n /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) & + - job: PublishNuget displayName: 'Publish NuGet packages' dependsOn: @@ -155,7 +158,7 @@ jobs: steps: - task: DotNetCoreCLI@2 displayName: 'Build Stable Nuget packages' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') inputs: command: 'pack' packagesToPack: 'Jellyfin.Data/Jellyfin.Data.csproj;MediaBrowser.Common/MediaBrowser.Common.csproj;MediaBrowser.Controller/MediaBrowser.Controller.csproj;MediaBrowser.Model/MediaBrowser.Model.csproj;Emby.Naming/Emby.Naming.csproj' @@ -172,7 +175,7 @@ jobs: MediaBrowser.Model/MediaBrowser.Model.csproj Emby.Naming/Emby.Naming.csproj custom: 'pack' - arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory)' + arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable' - task: PublishBuildArtifacts@1 displayName: 'Publish Nuget packages' @@ -180,10 +183,32 @@ jobs: pathToPublish: $(Build.ArtifactStagingDirectory) artifactName: Jellyfin Nuget Packages + - task: NuGetAuthenticate@0 + displayName: 'Authenticate to stable Nuget feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + nuGetServiceConnections: 'NugetOrg' + + - task: NuGetCommand@2 + displayName: 'Push Nuget packages to stable feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v') + inputs: + command: 'push' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg' + nuGetFeedType: 'external' + publishFeedCredentials: 'NugetOrg' + allowPackageConflicts: true # This ignores an error if the version already exists + + - task: NuGetAuthenticate@0 + displayName: 'Authenticate to unstable Nuget feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') + - task: NuGetCommand@2 - displayName: 'Push Nuget packages to feed' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + displayName: 'Push Nuget packages to unstable feed' + condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') inputs: command: 'push' - packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg' - includeNugetOrg: 'true' + packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' # No symbols since Azure Artifact does not support it + nuGetFeedType: 'internal' + publishVstsFeed: '7cce6c46-d610-45e3-9fb7-65a6bfd1b671/a5746b79-f369-42db-93ff-59cd066f9327' + allowPackageConflicts: true # This ignores an error if the version already exists diff --git a/.ci/azure-pipelines.yml b/.ci/azure-pipelines.yml index 0c86c0171..b417aae67 100644 --- a/.ci/azure-pipelines.yml +++ b/.ci/azure-pipelines.yml @@ -13,15 +13,21 @@ pr: trigger: batch: true + branches: + include: + - '*' + tags: + include: + - 'v*' jobs: -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(startsWith(variables['Build.SourceBranch'], 'refs/tags/v')) }}: - template: azure-pipelines-main.yml parameters: LinuxImage: 'ubuntu-latest' RestoreBuildProjects: $(RestoreBuildProjects) -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-test.yml parameters: ImageNames: @@ -29,7 +35,7 @@ jobs: Windows: 'windows-latest' macOS: 'macos-latest' -- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: +- ${{ if not(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master'))) }}: - template: azure-pipelines-abi.yml parameters: Packages: @@ -47,5 +53,5 @@ jobs: AssemblyFileName: MediaBrowser.Common.dll LinuxImage: 'ubuntu-latest' -- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: +- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}: - template: azure-pipelines-package.yml diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index c5f35c088..e383d02b7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -16,6 +16,7 @@ - [bugfixin](https://github.com/bugfixin) - [chaosinnovator](https://github.com/chaosinnovator) - [ckcr4lyf](https://github.com/ckcr4lyf) + - [ConfusedPolarBear](https://github.com/ConfusedPolarBear) - [crankdoofus](https://github.com/crankdoofus) - [crobibero](https://github.com/crobibero) - [cromefire](https://github.com/cromefire) @@ -77,6 +78,7 @@ - [nvllsvm](https://github.com/nvllsvm) - [nyanmisaka](https://github.com/nyanmisaka) - [oddstr13](https://github.com/oddstr13) + - [orryverducci](https://github.com/orryverducci) - [petermcneil](https://github.com/petermcneil) - [Phlogi](https://github.com/Phlogi) - [pjeanjean](https://github.com/pjeanjean) diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index be1ed7872..4b108b89e 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -1363,7 +1363,7 @@ namespace Emby.Dlna.ContentDirectory }; } - Logger.LogError("Error parsing item Id: {id}. Returning user root folder.", id); + Logger.LogError("Error parsing item Id: {Id}. Returning user root folder.", id); return new ServerItem(_libraryManager.GetUserRootFolder()); } diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index bd09a8051..5b8a89d8f 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -948,7 +948,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", name); + _logger.LogError(ex, "Error adding xml value: {Value}", name); } } @@ -960,7 +960,7 @@ namespace Emby.Dlna.Didl } catch (XmlException ex) { - _logger.LogError(ex, "Error adding xml value: {value}", value); + _logger.LogError(ex, "Error adding xml value: {Value}", value); } } diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 5e2c6e3e3..6857f9952 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -10,6 +10,15 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> <ItemGroup> @@ -28,6 +37,10 @@ <PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression> </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> + </ItemGroup> + <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <!-- TODO: <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> --> diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs index d4a8268b9..4ab0a2a3f 100644 --- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs +++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs @@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.AppBase } catch (Exception ex) { - Logger.LogError(ex, "Error loading configuration file: {path}", path); + Logger.LogError(ex, "Error loading configuration file: {Path}", path); return Activator.CreateInstance(configurationType); } diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index cb63cc85e..155bdcf65 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -37,10 +37,10 @@ using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Net; using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations.QuickConnect; using Emby.Server.Implementations.ScheduledTasks; using Emby.Server.Implementations.Security; using Emby.Server.Implementations.Serialization; -using Emby.Server.Implementations.Services; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; @@ -71,6 +71,7 @@ using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.QuickConnect; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; @@ -88,7 +89,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; using MediaBrowser.Model.System; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.Chapters; @@ -96,11 +96,12 @@ using MediaBrowser.Providers.Manager; using MediaBrowser.Providers.Plugins.TheTvdb; using MediaBrowser.Providers.Subtitles; using MediaBrowser.XbmcMetadata.Providers; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using OperatingSystem = MediaBrowser.Common.System.OperatingSystem; +using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations { @@ -121,14 +122,18 @@ namespace Emby.Server.Implementations private IMediaEncoder _mediaEncoder; private ISessionManager _sessionManager; - private IHttpServer _httpServer; + private IWebSocketManager _webSocketManager; private IHttpClient _httpClient; + private string[] _urlPrefixes; + /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> public bool CanSelfRestart => _startupOptions.RestartPath != null; + public bool CoreStartupHasCompleted { get; private set; } + public virtual bool CanLaunchWebBrowser { get @@ -274,6 +279,10 @@ namespace Emby.Server.Implementations Password = ServerConfigurationManager.Configuration.CertificatePassword }; Certificate = GetCertificate(CertificateInfo); + + ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; + ApplicationVersionString = ApplicationVersion.ToString(3); + ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; } public string ExpandVirtualPath(string path) @@ -303,16 +312,16 @@ namespace Emby.Server.Implementations } /// <inheritdoc /> - public Version ApplicationVersion { get; } = typeof(ApplicationHost).Assembly.GetName().Version; + public Version ApplicationVersion { get; } /// <inheritdoc /> - public string ApplicationVersionString { get; } = typeof(ApplicationHost).Assembly.GetName().Version.ToString(3); + public string ApplicationVersionString { get; } /// <summary> /// Gets the current application user agent. /// </summary> /// <value>The application user agent.</value> - public string ApplicationUserAgent => Name.Replace(' ', '-') + "/" + ApplicationVersionString; + public string ApplicationUserAgent { get; } /// <summary> /// Gets the email address for use within a comment section of a user agent field. @@ -443,8 +452,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed); Logger.LogInformation("Core startup complete"); - _httpServer.GlobalResponse = null; - + CoreStartupHasCompleted = true; stopWatch.Restart(); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed); @@ -499,9 +507,6 @@ namespace Emby.Server.Implementations RegisterServices(); } - public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next) - => _httpServer.RequestHandler(context); - /// <summary> /// Registers services/resources with the service collection that will be available via DI. /// </summary> @@ -541,8 +546,6 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<IZipClient, ZipClient>(); - ServiceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>(); - ServiceCollection.AddSingleton<IServerApplicationHost>(this); ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths); @@ -578,8 +581,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>(); - ServiceCollection.AddSingleton<ServiceController>(); - ServiceCollection.AddSingleton<IHttpServer, HttpListenerHost>(); + ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>(); ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>(); @@ -626,6 +628,7 @@ namespace Emby.Server.Implementations ServiceCollection.AddSingleton<ISessionContext, SessionContext>(); ServiceCollection.AddSingleton<IAuthService, AuthService>(); + ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>(); ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>(); @@ -651,7 +654,7 @@ namespace Emby.Server.Implementations _mediaEncoder = Resolve<IMediaEncoder>(); _sessionManager = Resolve<ISessionManager>(); - _httpServer = Resolve<IHttpServer>(); + _webSocketManager = Resolve<IWebSocketManager>(); _httpClient = Resolve<IHttpClient>(); ((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize(); @@ -753,7 +756,6 @@ namespace Emby.Server.Implementations CollectionFolder.XmlSerializer = _xmlSerializer; CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>(); CollectionFolder.ApplicationHost = this; - AuthenticatedAttribute.AuthService = Resolve<IAuthService>(); } /// <summary> @@ -773,7 +775,8 @@ namespace Emby.Server.Implementations .Where(i => i != null) .ToArray(); - _httpServer.Init(GetExportTypes<IService>(), GetExports<IWebSocketListener>(), GetUrlPrefixes()); + _urlPrefixes = GetUrlPrefixes().ToArray(); + _webSocketManager.Init(GetExports<IWebSocketListener>()); Resolve<ILibraryManager>().AddParts( GetExports<IResolverIgnoreRule>(), @@ -939,7 +942,7 @@ namespace Emby.Server.Implementations } } - if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) { requiresRestart = true; } @@ -1393,6 +1396,20 @@ namespace Emby.Server.Implementations _plugins = list.ToArray(); } + public IEnumerable<Assembly> GetApiPluginAssemblies() + { + var assemblies = _allConcreteTypes + .Where(i => typeof(ControllerBase).IsAssignableFrom(i)) + .Select(i => i.Assembly) + .Distinct(); + + foreach (var assembly in assemblies) + { + Logger.LogDebug("Found API endpoints in plugin {Name}", assembly.FullName); + yield return assembly; + } + } + public virtual void LaunchUrl(string url) { if (!CanLaunchWebBrowser) diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 26fc1bee4..fb1bb65a0 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -890,7 +890,7 @@ namespace Emby.Server.Implementations.Channels } catch (Exception ex) { - _logger.LogError(ex, "Error writing to channel cache file: {path}", path); + _logger.LogError(ex, "Error writing to channel cache file: {Path}", path); } } diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs index 64ccff53b..fde6fa115 100644 --- a/Emby.Server.Implementations/ConfigurationOptions.cs +++ b/Emby.Server.Implementations/ConfigurationOptions.cs @@ -15,7 +15,7 @@ namespace Emby.Server.Implementations public static Dictionary<string, string> DefaultConfiguration => new Dictionary<string, string> { { HostWebClientKey, bool.TrueString }, - { HttpListenerHost.DefaultRedirectKey, "web/index.html" }, + { DefaultRedirectKey, "web/index.html" }, { FfmpegProbeSizeKey, "1G" }, { FfmpegAnalyzeDurationKey, "200M" }, { PlaylistsAllowDuplicatesKey, bool.TrueString }, diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index f2c7118fe..57c1398e9 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -197,7 +197,7 @@ namespace Emby.Server.Implementations.Dto catch (Exception ex) { // Have to use a catch-all unfortunately because some .net image methods throw plain Exceptions - _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {itemName}", item.Name); + _logger.LogError(ex, "Error generating PrimaryImageAspectRatio for {ItemName}", item.Name); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 60564f700..0a348f0d0 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="IPNetwork2" Version="2.5.211" /> + <PackageReference Include="IPNetwork2" Version="2.5.224" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" /> <PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" /> @@ -32,12 +32,12 @@ <PackageReference Include="Microsoft.AspNetCore.ResponseCompression" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.7" /> <PackageReference Include="Mono.Nat" Version="2.0.2" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.3.1" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="3.4.0" /> <PackageReference Include="ServiceStack.Text.Core" Version="5.9.2" /> <PackageReference Include="sharpcompress" Version="0.26.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="2.1.0" /> diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs deleted file mode 100644 index 6fce8de44..000000000 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ /dev/null @@ -1,250 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class FileWriter : IHttpResult - { - private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); - - private static readonly string[] _skipLogExtensions = { - ".js", - ".html", - ".css" - }; - - private readonly IStreamHelper _streamHelper; - private readonly ILogger _logger; - - /// <summary> - /// The _options. - /// </summary> - private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); - - /// <summary> - /// The _requested ranges. - /// </summary> - private List<KeyValuePair<long, long?>> _requestedRanges; - - public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - _streamHelper = streamHelper; - - Path = path; - _logger = logger; - RangeHeader = rangeHeader; - - Headers[HeaderNames.ContentType] = contentType; - - TotalContentLength = fileSystem.GetFileInfo(path).Length; - Headers[HeaderNames.AcceptRanges] = "bytes"; - - if (string.IsNullOrWhiteSpace(rangeHeader)) - { - Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); - StatusCode = HttpStatusCode.OK; - } - else - { - StatusCode = HttpStatusCode.PartialContent; - SetRangeValues(); - } - - FileShare = FileShare.Read; - Cookies = new List<Cookie>(); - } - - private string RangeHeader { get; set; } - - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - - private long RangeEnd { get; set; } - - private long RangeLength { get; set; } - - public long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public List<Cookie> Cookies { get; private set; } - - public FileShare FileShare { get; set; } - - /// <summary> - /// Gets the options. - /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Headers => _options; - - public string Path { get; set; } - - /// <summary> - /// Gets the requested ranges. - /// </summary> - /// <value>The requested ranges.</value> - protected List<KeyValuePair<long, long?>> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List<KeyValuePair<long, long?>>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], UsCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], UsCulture); - } - - _requestedRanges.Add(new KeyValuePair<long, long?>(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// <summary> - /// Sets the range values. - /// </summary> - private void SetRangeValues() - { - var requestedRange = RequestedRanges[0]; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - // Content-Length is the length of what we're serving, not the original content - var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentLength] = lengthString; - var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - Headers[HeaderNames.ContentRange] = rangeString; - - _logger.LogDebug("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); - } - - public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - var path = Path; - var offset = RangeStart; - var count = RangeLength; - - if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) - { - var extension = System.IO.Path.GetExtension(path); - - if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) - { - _logger.LogDebug("Transmit file {0}", path); - } - - offset = 0; - count = 0; - } - - await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); - } - finally - { - OnComplete?.Invoke(); - } - } - - public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShare fileShare, CancellationToken cancellationToken) - { - var fileOptions = FileOptions.SequentialScan; - - // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - fileOptions |= FileOptions.Asynchronous; - } - - using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, fileShare, IODefaults.FileStreamBufferSize, fileOptions)) - { - if (offset > 0) - { - fs.Position = offset; - } - - if (count > 0) - { - await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); - } - else - { - await fs.CopyToAsync(stream, IODefaults.CopyToBufferSize, cancellationToken).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs deleted file mode 100644 index fe39bb4b2..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ /dev/null @@ -1,766 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net.Sockets; -using System.Net.WebSockets; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.Services; -using Emby.Server.Implementations.SocketSharp; -using Jellyfin.Data.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Globalization; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using ServiceStack.Text.Jsv; - -namespace Emby.Server.Implementations.HttpServer -{ - public class HttpListenerHost : IHttpServer - { - /// <summary> - /// The key for a setting that specifies the default redirect path - /// to use for requests where the URL base prefix is invalid or missing. - /// </summary> - public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath"; - - private readonly ILogger<HttpListenerHost> _logger; - private readonly ILoggerFactory _loggerFactory; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; - private readonly IServerApplicationHost _appHost; - private readonly IJsonSerializer _jsonSerializer; - private readonly IXmlSerializer _xmlSerializer; - private readonly Func<Type, Func<string, object>> _funcParseFn; - private readonly string _defaultRedirectPath; - private readonly string _baseUrlPrefix; - - private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>(); - private readonly IHostEnvironment _hostEnvironment; - - private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); - private bool _disposed = false; - - public HttpListenerHost( - IServerApplicationHost applicationHost, - ILogger<HttpListenerHost> logger, - IServerConfigurationManager config, - IConfiguration configuration, - INetworkManager networkManager, - IJsonSerializer jsonSerializer, - IXmlSerializer xmlSerializer, - ILocalizationManager localizationManager, - ServiceController serviceController, - IHostEnvironment hostEnvironment, - ILoggerFactory loggerFactory) - { - _appHost = applicationHost; - _logger = logger; - _config = config; - _defaultRedirectPath = configuration[DefaultRedirectKey]; - _baseUrlPrefix = _config.Configuration.BaseUrl; - _networkManager = networkManager; - _jsonSerializer = jsonSerializer; - _xmlSerializer = xmlSerializer; - ServiceController = serviceController; - _hostEnvironment = hostEnvironment; - _loggerFactory = loggerFactory; - - _funcParseFn = t => s => JsvReader.GetParseFn(t)(s); - - Instance = this; - ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>(); - GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); - } - - public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - - public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; } - - public static HttpListenerHost Instance { get; protected set; } - - public string[] UrlPrefixes { get; private set; } - - public string GlobalResponse { get; set; } - - public ServiceController ServiceController { get; } - - public object CreateInstance(Type type) - { - return _appHost.CreateInstance(type); - } - - private static string NormalizeUrlPath(string path) - { - if (path.Length > 0 && path[0] == '/') - { - // If the path begins with a leading slash, just return it as-is - return path; - } - else - { - // If the path does not begin with a leading slash, append one for consistency - return "/" + path; - } - } - - /// <summary> - /// Applies the request filters. Returns whether or not the request has been handled - /// and no more processing should be done. - /// </summary> - /// <returns></returns> - public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto) - { - // Exec all RequestFilter attributes with Priority < 0 - var attributes = GetRequestFilterAttributes(requestDto.GetType()); - - int count = attributes.Count; - int i = 0; - for (; i < count && attributes[i].Priority < 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - - // Exec remaining RequestFilter attributes with Priority >= 0 - for (; i < count && attributes[i].Priority >= 0; i++) - { - var attribute = attributes[i]; - attribute.RequestFilter(req, res, requestDto); - } - } - - public Type GetServiceTypeByRequest(Type requestType) - { - _serviceOperationsMap.TryGetValue(requestType, out var serviceType); - return serviceType; - } - - public void AddServiceInfo(Type serviceType, Type requestType) - { - _serviceOperationsMap[requestType] = serviceType; - } - - private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType) - { - var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList(); - - var serviceType = GetServiceTypeByRequest(requestDtoType); - if (serviceType != null) - { - attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>()); - } - - attributes.Sort((x, y) => x.Priority - y.Priority); - - return attributes; - } - - private static Exception GetActualException(Exception ex) - { - if (ex is AggregateException agg) - { - var inner = agg.InnerException; - if (inner != null) - { - return GetActualException(inner); - } - else - { - var inners = agg.InnerExceptions; - if (inners.Count > 0) - { - return GetActualException(inners[0]); - } - } - } - - return ex; - } - - private int GetStatusCode(Exception ex) - { - switch (ex) - { - case ArgumentException _: return 400; - case AuthenticationException _: return 401; - case SecurityException _: return 403; - case DirectoryNotFoundException _: - case FileNotFoundException _: - case ResourceNotFoundException _: return 404; - case MethodNotAllowedException _: return 405; - default: return 500; - } - } - - private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace) - { - if (ignoreStackTrace) - { - _logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog); - } - else - { - _logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog); - } - - var httpRes = httpReq.Response; - - if (httpRes.HasStarted) - { - return; - } - - httpRes.StatusCode = statusCode; - - var errContent = _hostEnvironment.IsDevelopment() - ? (NormalizeExceptionMessage(ex) ?? string.Empty) - : "Error processing request."; - httpRes.ContentType = "text/plain"; - httpRes.ContentLength = errContent.Length; - await httpRes.WriteAsync(errContent).ConfigureAwait(false); - } - - private string NormalizeExceptionMessage(Exception ex) - { - // Do not expose the exception message for AuthenticationException - if (ex is AuthenticationException) - { - return null; - } - - // Strip any information we don't want to reveal - return ex.Message - ?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase) - .Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase); - } - - public static string RemoveQueryStringByKey(string url, string key) - { - var uri = new Uri(url); - - // this gets all the query string key value pairs as a collection - var newQueryString = QueryHelpers.ParseQuery(uri.Query); - - var originalCount = newQueryString.Count; - - if (originalCount == 0) - { - return url; - } - - // this removes the key if exists - newQueryString.Remove(key); - - if (originalCount == newQueryString.Count) - { - return url; - } - - // this gets the page path from root without QueryString - string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; - - return newQueryString.Count > 0 - ? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString())) - : pagePathWithoutQueryString; - } - - private static string GetUrlToLog(string url) - { - url = RemoveQueryStringByKey(url, "api_key"); - - return url; - } - - private static string NormalizeConfiguredLocalAddress(string address) - { - var add = address.AsSpan().Trim('/'); - int index = add.IndexOf('/'); - if (index != -1) - { - add = add.Slice(index + 1); - } - - return add.TrimStart('/').ToString(); - } - - private bool ValidateHost(string host) - { - var hosts = _config - .Configuration - .LocalNetworkAddresses - .Select(NormalizeConfiguredLocalAddress) - .ToList(); - - if (hosts.Count == 0) - { - return true; - } - - host ??= string.Empty; - - if (_networkManager.IsInPrivateAddressSpace(host)) - { - hosts.Add("localhost"); - hosts.Add("127.0.0.1"); - - return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); - } - - return true; - } - - private bool ValidateRequest(string remoteIp, bool isLocal) - { - if (isLocal) - { - return true; - } - - if (_config.Configuration.EnableRemoteAccess) - { - var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); - - if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp)) - { - if (_config.Configuration.IsRemoteIPFilterBlacklist) - { - return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - else - { - return _networkManager.IsAddressInSubnets(remoteIp, addressFilter); - } - } - } - else - { - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// <summary> - /// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required. - /// </summary> - /// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns> - private bool ValidateSsl(string remoteIp, string urlString) - { - if (_config.Configuration.RequireHttps - && _appHost.ListenWithHttps - && !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase)) - { - // These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected - if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1 - || urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1) - { - return true; - } - - if (!_networkManager.IsInLocalNetwork(remoteIp)) - { - return false; - } - } - - return true; - } - - /// <inheritdoc /> - public Task RequestHandler(HttpContext context) - { - if (context.WebSockets.IsWebSocketRequest) - { - return WebSocketRequestHandler(context); - } - - var request = context.Request; - var response = context.Response; - var localPath = context.Request.Path.ToString(); - - var req = new WebSocketSharpRequest(request, response, request.Path); - return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted); - } - - /// <summary> - /// Overridable method that can be used to implement a custom handler. - /// </summary> - private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken) - { - var stopWatch = new Stopwatch(); - stopWatch.Start(); - var httpRes = httpReq.Response; - string urlToLog = GetUrlToLog(urlString); - string remoteIp = httpReq.RemoteIp; - - try - { - if (_disposed) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateHost(host)) - { - httpRes.StatusCode = 400; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateRequest(remoteIp, httpReq.IsLocal)) - { - httpRes.StatusCode = 403; - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false); - return; - } - - if (!ValidateSsl(httpReq.RemoteIp, urlString)) - { - RedirectToSecureUrl(httpReq, httpRes, urlString); - return; - } - - if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) - { - httpRes.StatusCode = 200; - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - httpRes.Headers.Add(key, value); - } - - httpRes.ContentType = "text/plain"; - await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false); - return; - } - - if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase) - || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(localPath) - || !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - // Always redirect back to the default path if the base prefix is invalid or missing - _logger.LogDebug("Normalizing a URL at {0}", localPath); - httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath); - return; - } - - if (!string.IsNullOrEmpty(GlobalResponse)) - { - // We don't want the address pings in ApplicationHost to fail - if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) - { - httpRes.StatusCode = 503; - httpRes.ContentType = "text/html"; - await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false); - return; - } - } - - var handler = GetServiceHandler(httpReq); - if (handler != null) - { - await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false); - } - else - { - throw new FileNotFoundException(); - } - } - catch (Exception requestEx) - { - try - { - var requestInnerEx = GetActualException(requestEx); - var statusCode = GetStatusCode(requestInnerEx); - - foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) - { - if (!httpRes.Headers.ContainsKey(key)) - { - httpRes.Headers.Add(key, value); - } - } - - bool ignoreStackTrace = - requestInnerEx is SocketException - || requestInnerEx is IOException - || requestInnerEx is OperationCanceledException - || requestInnerEx is SecurityException - || requestInnerEx is AuthenticationException - || requestInnerEx is FileNotFoundException; - - // Do not handle 500 server exceptions manually when in development mode. - // Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware. - // However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored, - // because it will log the stack trace when it handles the exception. - if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment()) - { - throw; - } - - await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false); - } - catch (Exception handlerException) - { - var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException); - _logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog); - - if (_hostEnvironment.IsDevelopment()) - { - throw aggregateEx; - } - } - } - finally - { - if (httpRes.StatusCode >= 500) - { - _logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog); - } - - stopWatch.Stop(); - var elapsed = stopWatch.Elapsed; - if (elapsed.TotalMilliseconds > 500) - { - _logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog); - } - } - } - - private async Task WebSocketRequestHandler(HttpContext context) - { - if (_disposed) - { - return; - } - - try - { - _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); - - WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); - - using var connection = new WebSocketConnection( - _loggerFactory.CreateLogger<WebSocketConnection>(), - webSocket, - context.Connection.RemoteIpAddress, - context.Request.Query) - { - OnReceive = ProcessWebSocketMessageReceived - }; - - WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); - - await connection.ProcessAsync().ConfigureAwait(false); - _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); - } - catch (Exception ex) // Otherwise ASP.Net will ignore the exception - { - _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); - if (!context.Response.HasStarted) - { - context.Response.StatusCode = 500; - } - } - } - - /// <summary> - /// Get the default CORS headers. - /// </summary> - /// <param name="req"></param> - /// <returns></returns> - public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req) - { - var origin = req.Headers["Origin"]; - if (origin == StringValues.Empty) - { - origin = req.Headers["Host"]; - if (origin == StringValues.Empty) - { - origin = "*"; - } - } - - var headers = new Dictionary<string, string>(); - headers.Add("Access-Control-Allow-Origin", origin); - headers.Add("Access-Control-Allow-Credentials", "true"); - headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); - headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie"); - return headers; - } - - // Entry point for HttpListener - public ServiceHandler GetServiceHandler(IHttpRequest httpReq) - { - var pathInfo = httpReq.PathInfo; - - pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType); - var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo); - if (restPath != null) - { - return new ServiceHandler(restPath, contentType); - } - - _logger.LogError("Could not find handler for {PathInfo}", pathInfo); - return null; - } - - private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url) - { - if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - var builder = new UriBuilder(uri) - { - Port = _config.Configuration.PublicHttpsPort, - Scheme = "https" - }; - url = builder.Uri.ToString(); - } - - httpRes.Redirect(url); - } - - /// <summary> - /// Adds the rest handlers. - /// </summary> - /// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param> - /// <param name="listeners">The web socket listeners.</param> - /// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param> - public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes) - { - _webSocketListeners = listeners.ToArray(); - UrlPrefixes = urlPrefixes.ToArray(); - - ServiceController.Init(this, serviceTypes); - - ResponseFilters = new Action<IRequest, HttpResponse, object>[] - { - new ResponseFilter(this, _logger).FilterResponse - }; - } - - public RouteAttribute[] GetRouteAttributes(Type requestType) - { - var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList(); - var clone = routes.ToList(); - - foreach (var route in clone) - { - routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - - routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs) - { - Notes = route.Notes, - Priority = route.Priority, - Summary = route.Summary - }); - } - - return routes.ToArray(); - } - - public Func<string, object> GetParseFn(Type propertyType) - { - return _funcParseFn(propertyType); - } - - public void SerializeToJson(object o, Stream stream) - { - _jsonSerializer.SerializeToStream(o, stream); - } - - public void SerializeToXml(object o, Stream stream) - { - _xmlSerializer.SerializeToStream(o, stream); - } - - public Task<object> DeserializeXml(Type type, Stream stream) - { - return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream)); - } - - public Task<object> DeserializeJson(Type type, Stream stream) - { - return _jsonSerializer.DeserializeFromStreamAsync(stream, type); - } - - private string NormalizeEmbyRoutePath(string path) - { - _logger.LogDebug("Normalizing /emby route"); - return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path); - } - - private string NormalizeMediaBrowserRoutePath(string path) - { - _logger.LogDebug("Normalizing /mediabrowser route"); - return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path); - } - - private string NormalizeCustomRoutePath(string path) - { - _logger.LogDebug("Normalizing custom route {0}", path); - return _baseUrlPrefix + NormalizeUrlPath(path); - } - - /// <summary> - /// Processes the web socket message received. - /// </summary> - /// <param name="result">The result.</param> - private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) - { - if (_disposed) - { - return Task.CompletedTask; - } - - IEnumerable<Task> GetTasks() - { - foreach (var x in _webSocketListeners) - { - yield return x.ProcessMessageAsync(result); - } - } - - return Task.WhenAll(GetTasks()); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs deleted file mode 100644 index 688216373..000000000 --- a/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs +++ /dev/null @@ -1,721 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.IO.Compression; -using System.Net; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -using System.Xml; -using Emby.Server.Implementations.Services; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.IO; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IRequest = MediaBrowser.Model.Services.IRequest; -using MimeTypes = MediaBrowser.Model.Net.MimeTypes; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class HttpResultFactory. - /// </summary> - public class HttpResultFactory : IHttpResultFactory - { - // Last-Modified and If-Modified-Since must follow strict date format, - // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since - private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\""; - // We specifically use en-US culture because both day of week and month names require it - private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false); - - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<HttpResultFactory> _logger; - private readonly IFileSystem _fileSystem; - private readonly IJsonSerializer _jsonSerializer; - private readonly IStreamHelper _streamHelper; - - /// <summary> - /// Initializes a new instance of the <see cref="HttpResultFactory" /> class. - /// </summary> - public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper) - { - _fileSystem = fileSystem; - _jsonSerializer = jsonSerializer; - _streamHelper = streamHelper; - _logger = loggerfactory.CreateLogger<HttpResultFactory>(); - } - - /// <summary> - /// Gets the result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="content">The content.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(null, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null) - { - return GetHttpResult(requestContext, content, contentType, true, responseHeaders); - } - - public object GetRedirectResult(string url) - { - var responseHeaders = new Dictionary<string, string>(); - responseHeaders[HeaderNames.Location] = url; - - var result = new HttpResult(Array.Empty<byte>(), "text/plain", HttpStatusCode.Redirect); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - var result = new StreamWriter(content, contentType); - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - string compressionType = null; - bool isHeadRequest = false; - - if (requestContext != null) - { - compressionType = GetCompressionType(requestContext, content, contentType); - isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - } - - IHasHeaders result; - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = content.Length; - - if (isHeadRequest) - { - content = Array.Empty<byte>(); - } - - result = new StreamWriter(content, contentType, contentLength); - } - else - { - result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the HTTP result. - /// </summary> - private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary<string, string> responseHeaders = null) - { - IHasHeaders result; - - var bytes = Encoding.UTF8.GetBytes(content); - - var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType); - - var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase); - - if (string.IsNullOrEmpty(compressionType)) - { - var contentLength = bytes.Length; - - if (isHeadRequest) - { - bytes = Array.Empty<byte>(); - } - - result = new StreamWriter(bytes, contentType, contentLength); - } - else - { - result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(); - } - - if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _)) - { - responseHeaders[HeaderNames.Expires] = "0"; - } - - AddResponseHeaders(result, responseHeaders); - - return result; - } - - /// <summary> - /// Gets the optimized result. - /// </summary> - /// <typeparam name="T"></typeparam> - public object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) - where T : class - { - if (result == null) - { - throw new ArgumentNullException(nameof(result)); - } - - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - responseHeaders[HeaderNames.Expires] = "0"; - - return ToOptimizedResultInternal(requestContext, result, responseHeaders); - } - - private string GetCompressionType(IRequest request, byte[] content, string responseContentType) - { - if (responseContentType == null) - { - return null; - } - - // Per apple docs, hls manifests must be compressed - if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && - responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 && - responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1) - { - return null; - } - - if (content.Length < 1024) - { - return null; - } - - return GetCompressionType(request); - } - - private static string GetCompressionType(IRequest request) - { - var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString(); - - if (!string.IsNullOrEmpty(acceptEncoding)) - { - // if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1) - // return "br"; - - if (acceptEncoding.Contains("deflate", StringComparison.OrdinalIgnoreCase)) - { - return "deflate"; - } - - if (acceptEncoding.Contains("gzip", StringComparison.OrdinalIgnoreCase)) - { - return "gzip"; - } - } - - return null; - } - - /// <summary> - /// Returns the optimized result for the IRequestContext. - /// Does not use or store results in any cache. - /// </summary> - /// <param name="request"></param> - /// <param name="dto"></param> - /// <returns></returns> - public object ToOptimizedResult<T>(IRequest request, T dto) - { - return ToOptimizedResultInternal(request, dto); - } - - private object ToOptimizedResultInternal<T>(IRequest request, T dto, IDictionary<string, string> responseHeaders = null) - { - // TODO: @bond use Span and .Equals - var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant(); - - switch (contentType) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders); - - case "application/json": - case "text/json": - return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders); - default: - break; - } - - var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase); - - var ms = new MemoryStream(); - var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - writerFn(dto, ms); - - ms.Position = 0; - - if (isHeadRequest) - { - using (ms) - { - return GetHttpResult(request, Array.Empty<byte>(), contentType, true, responseHeaders); - } - } - - return GetHttpResult(request, ms, contentType, true, responseHeaders); - } - - private IHasHeaders GetCompressedResult( - byte[] content, - string requestedCompressionType, - IDictionary<string, string> responseHeaders, - bool isHeadRequest, - string contentType) - { - if (responseHeaders == null) - { - responseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - } - - content = Compress(content, requestedCompressionType); - responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType; - - responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding; - - var contentLength = content.Length; - - if (isHeadRequest) - { - var result = new StreamWriter(Array.Empty<byte>(), contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - else - { - var result = new StreamWriter(content, contentType, contentLength); - AddResponseHeaders(result, responseHeaders); - return result; - } - } - - private byte[] Compress(byte[] bytes, string compressionType) - { - if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase)) - { - return Deflate(bytes); - } - - if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase)) - { - return GZip(bytes); - } - - throw new NotSupportedException(compressionType); - } - - private static byte[] Deflate(byte[] bytes) - { - // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream - // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream - using (var ms = new MemoryStream()) - using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) - { - zipStream.Write(bytes, 0, bytes.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static byte[] GZip(byte[] buffer) - { - using (var ms = new MemoryStream()) - using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) - { - zipStream.Write(buffer, 0, buffer.Length); - zipStream.Dispose(); - - return ms.ToArray(); - } - } - - private static string SerializeToXmlString(object from) - { - using (var ms = new MemoryStream()) - { - var xwSettings = new XmlWriterSettings(); - xwSettings.Encoding = new UTF8Encoding(false); - xwSettings.OmitXmlDeclaration = false; - - using (var xw = XmlWriter.Create(ms, xwSettings)) - { - var serializer = new DataContractSerializer(from.GetType()); - serializer.WriteObject(xw, from); - xw.Flush(); - ms.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(ms)) - { - return reader.ReadToEnd(); - } - } - } - } - - /// <summary> - /// Pres the process optimized result. - /// </summary> - private object GetCachedResult(IRequest requestContext, IDictionary<string, string> responseHeaders, StaticResultOptions options) - { - bool noCache = requestContext.Headers[HeaderNames.CacheControl].ToString().IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1; - AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified); - - if (!noCache) - { - if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader)) - { - _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]); - return null; - } - - if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified)) - { - AddAgeHeader(responseHeaders, options.DateLastModified); - - var result = new HttpResult(Array.Empty<byte>(), options.ContentType ?? "text/html", HttpStatusCode.NotModified); - - AddResponseHeaders(result, responseHeaders); - - return result; - } - } - - return null; - } - - public Task<object> GetStaticFileResult(IRequest requestContext, - string path, - FileShare fileShare = FileShare.Read) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - return GetStaticFileResult(requestContext, new StaticFileResultOptions - { - Path = path, - FileShare = fileShare - }); - } - - public Task<object> GetStaticFileResult(IRequest requestContext, StaticFileResultOptions options) - { - var path = options.Path; - var fileShare = options.FileShare; - - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentException("Path can't be empty.", nameof(options)); - } - - if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) - { - throw new ArgumentException("FileShare must be either Read or ReadWrite"); - } - - if (string.IsNullOrEmpty(options.ContentType)) - { - options.ContentType = MimeTypes.GetMimeType(path); - } - - if (!options.DateLastModified.HasValue) - { - options.DateLastModified = _fileSystem.GetLastWriteTimeUtc(path); - } - - options.ContentFactory = () => Task.FromResult(GetFileStream(path, fileShare)); - - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - return GetStaticResult(requestContext, options); - } - - /// <summary> - /// Gets the file stream. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="fileShare">The file share.</param> - /// <returns>Stream.</returns> - private Stream GetFileStream(string path, FileShare fileShare) - { - return new FileStream(path, FileMode.Open, FileAccess.Read, fileShare); - } - - public Task<object> GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, - Func<Task<Stream>> factoryFn, - IDictionary<string, string> responseHeaders = null, - bool isHeadRequest = false) - { - return GetStaticResult(requestContext, new StaticResultOptions - { - CacheDuration = cacheDuration, - ContentFactory = factoryFn, - ContentType = contentType, - DateLastModified = lastDateModified, - IsHeadRequest = isHeadRequest, - ResponseHeaders = responseHeaders - }); - } - - public async Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options) - { - options.ResponseHeaders = options.ResponseHeaders ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - - var contentType = options.ContentType; - if (!StringValues.IsNullOrEmpty(requestContext.Headers[HeaderNames.IfModifiedSince])) - { - // See if the result is already cached in the browser - var result = GetCachedResult(requestContext, options.ResponseHeaders, options); - - if (result != null) - { - return result; - } - } - - // TODO: We don't really need the option value - var isHeadRequest = options.IsHeadRequest || string.Equals(requestContext.Verb, "HEAD", StringComparison.OrdinalIgnoreCase); - var factoryFn = options.ContentFactory; - var responseHeaders = options.ResponseHeaders; - AddCachingHeaders(responseHeaders, options.CacheDuration, false, options.DateLastModified); - AddAgeHeader(responseHeaders, options.DateLastModified); - - var rangeHeader = requestContext.Headers[HeaderNames.Range]; - - if (!isHeadRequest && !string.IsNullOrEmpty(options.Path)) - { - var hasHeaders = new FileWriter(options.Path, contentType, rangeHeader, _logger, _fileSystem, _streamHelper) - { - OnComplete = options.OnComplete, - OnError = options.OnError, - FileShare = options.FileShare - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - - var stream = await factoryFn().ConfigureAwait(false); - - var totalContentLength = options.ContentLength; - if (!totalContentLength.HasValue) - { - try - { - totalContentLength = stream.Length; - } - catch (NotSupportedException) - { - } - } - - if (!string.IsNullOrWhiteSpace(rangeHeader) && totalContentLength.HasValue) - { - var hasHeaders = new RangeRequestWriter(rangeHeader, totalContentLength.Value, stream, contentType, isHeadRequest) - { - OnComplete = options.OnComplete - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - else - { - if (totalContentLength.HasValue) - { - responseHeaders["Content-Length"] = totalContentLength.Value.ToString(CultureInfo.InvariantCulture); - } - - if (isHeadRequest) - { - using (stream) - { - return GetHttpResult(requestContext, Array.Empty<byte>(), contentType, true, responseHeaders); - } - } - - var hasHeaders = new StreamWriter(stream, contentType) - { - OnComplete = options.OnComplete, - OnError = options.OnError - }; - - AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasHeaders; - } - } - - /// <summary> - /// Adds the caching responseHeaders. - /// </summary> - private void AddCachingHeaders( - IDictionary<string, string> responseHeaders, - TimeSpan? cacheDuration, - bool noCache, - DateTime? lastModifiedDate) - { - if (noCache) - { - responseHeaders[HeaderNames.CacheControl] = "no-cache, no-store, must-revalidate"; - responseHeaders[HeaderNames.Pragma] = "no-cache, no-store, must-revalidate"; - return; - } - - if (cacheDuration.HasValue) - { - responseHeaders[HeaderNames.CacheControl] = "public, max-age=" + cacheDuration.Value.TotalSeconds; - } - else - { - responseHeaders[HeaderNames.CacheControl] = "public"; - } - - if (lastModifiedDate.HasValue) - { - responseHeaders[HeaderNames.LastModified] = lastModifiedDate.Value.ToUniversalTime().ToString(HttpDateFormat, _enUSculture); - } - } - - /// <summary> - /// Adds the age header. - /// </summary> - /// <param name="responseHeaders">The responseHeaders.</param> - /// <param name="lastDateModified">The last date modified.</param> - private static void AddAgeHeader(IDictionary<string, string> responseHeaders, DateTime? lastDateModified) - { - if (lastDateModified.HasValue) - { - responseHeaders[HeaderNames.Age] = Convert.ToInt64((DateTime.UtcNow - lastDateModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture); - } - } - - /// <summary> - /// Determines whether [is not modified] [the specified if modified since]. - /// </summary> - /// <param name="ifModifiedSince">If modified since.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="dateModified">The date modified.</param> - /// <returns><c>true</c> if [is not modified] [the specified if modified since]; otherwise, <c>false</c>.</returns> - private bool IsNotModified(DateTime ifModifiedSince, TimeSpan? cacheDuration, DateTime? dateModified) - { - if (dateModified.HasValue) - { - var lastModified = NormalizeDateForComparison(dateModified.Value); - ifModifiedSince = NormalizeDateForComparison(ifModifiedSince); - - return lastModified <= ifModifiedSince; - } - - if (cacheDuration.HasValue) - { - var cacheExpirationDate = ifModifiedSince.Add(cacheDuration.Value); - - if (DateTime.UtcNow < cacheExpirationDate) - { - return true; - } - } - - return false; - } - - - /// <summary> - /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that. - /// </summary> - /// <param name="date">The date.</param> - /// <returns>DateTime.</returns> - private static DateTime NormalizeDateForComparison(DateTime date) - { - return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind); - } - - /// <summary> - /// Adds the response headers. - /// </summary> - /// <param name="hasHeaders">The has options.</param> - /// <param name="responseHeaders">The response headers.</param> - private static void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable<KeyValuePair<string, string>> responseHeaders) - { - foreach (var item in responseHeaders) - { - hasHeaders.Headers[item.Key] = item.Value; - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs deleted file mode 100644 index 980c2cd3a..000000000 --- a/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Buffers; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult - { - private const int BufferSize = 81920; - - private readonly Dictionary<string, string> _options = new Dictionary<string, string>(); - - private List<KeyValuePair<long, long?>> _requestedRanges; - - /// <summary> - /// Initializes a new instance of the <see cref="RangeRequestWriter" /> class. - /// </summary> - /// <param name="rangeHeader">The range header.</param> - /// <param name="contentLength">The content length.</param> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> - public RangeRequestWriter(string rangeHeader, long contentLength, Stream source, string contentType, bool isHeadRequest) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - RangeHeader = rangeHeader; - SourceStream = source; - IsHeadRequest = isHeadRequest; - - ContentType = contentType; - Headers[HeaderNames.ContentType] = contentType; - Headers[HeaderNames.AcceptRanges] = "bytes"; - StatusCode = HttpStatusCode.PartialContent; - - SetRangeValues(contentLength); - } - - /// <summary> - /// Gets or sets the source stream. - /// </summary> - /// <value>The source stream.</value> - private Stream SourceStream { get; set; } - private string RangeHeader { get; set; } - private bool IsHeadRequest { get; set; } - - private long RangeStart { get; set; } - private long RangeEnd { get; set; } - private long RangeLength { get; set; } - private long TotalContentLength { get; set; } - - public Action OnComplete { get; set; } - - /// <summary> - /// Additional HTTP Headers - /// </summary> - /// <value>The headers.</value> - public IDictionary<string, string> Headers => _options; - - /// <summary> - /// Gets the requested ranges. - /// </summary> - /// <value>The requested ranges.</value> - protected List<KeyValuePair<long, long?>> RequestedRanges - { - get - { - if (_requestedRanges == null) - { - _requestedRanges = new List<KeyValuePair<long, long?>>(); - - // Example: bytes=0-,32-63 - var ranges = RangeHeader.Split('=')[1].Split(','); - - foreach (var range in ranges) - { - var vals = range.Split('-'); - - long start = 0; - long? end = null; - - if (!string.IsNullOrEmpty(vals[0])) - { - start = long.Parse(vals[0], CultureInfo.InvariantCulture); - } - - if (!string.IsNullOrEmpty(vals[1])) - { - end = long.Parse(vals[1], CultureInfo.InvariantCulture); - } - - _requestedRanges.Add(new KeyValuePair<long, long?>(start, end)); - } - } - - return _requestedRanges; - } - } - - public string ContentType { get; set; } - - public IRequest RequestContext { get; set; } - - public object Response { get; set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - /// <summary> - /// Sets the range values. - /// </summary> - private void SetRangeValues(long contentLength) - { - var requestedRange = RequestedRanges[0]; - - TotalContentLength = contentLength; - - // If the requested range is "0-", we can optimize by just doing a stream copy - if (!requestedRange.Value.HasValue) - { - RangeEnd = TotalContentLength - 1; - } - else - { - RangeEnd = requestedRange.Value.Value; - } - - RangeStart = requestedRange.Key; - RangeLength = 1 + RangeEnd - RangeStart; - - Headers[HeaderNames.ContentLength] = RangeLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentRange] = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; - - if (RangeStart > 0 && SourceStream.CanSeek) - { - SourceStream.Position = RangeStart; - } - } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - using (var source = SourceStream) - { - // If the requested range is "0-", we can optimize by just doing a stream copy - if (RangeEnd >= TotalContentLength - 1) - { - await source.CopyToAsync(responseStream, BufferSize, cancellationToken).ConfigureAwait(false); - } - else - { - await CopyToInternalAsync(source, responseStream, RangeLength, cancellationToken).ConfigureAwait(false); - } - } - } - finally - { - OnComplete?.Invoke(); - } - } - - private static async Task CopyToInternalAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - var array = ArrayPool<byte>.Shared.Rent(BufferSize); - try - { - int bytesRead; - while ((bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToCopy = Math.Min(bytesRead, copyLength); - - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToCopy), cancellationToken).ConfigureAwait(false); - - copyLength -= bytesToCopy; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(array); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs deleted file mode 100644 index a8cd2ac8f..000000000 --- a/Emby.Server.Implementations/HttpServer/ResponseFilter.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Globalization; -using System.Text; -using MediaBrowser.Controller.Net; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class ResponseFilter. - /// </summary> - public class ResponseFilter - { - private readonly IHttpServer _server; - private readonly ILogger _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="ResponseFilter"/> class. - /// </summary> - /// <param name="server">The HTTP server.</param> - /// <param name="logger">The logger.</param> - public ResponseFilter(IHttpServer server, ILogger logger) - { - _server = server; - _logger = logger; - } - - /// <summary> - /// Filters the response. - /// </summary> - /// <param name="req">The req.</param> - /// <param name="res">The res.</param> - /// <param name="dto">The dto.</param> - public void FilterResponse(IRequest req, HttpResponse res, object dto) - { - foreach(var (key, value) in _server.GetDefaultCorsHeaders(req)) - { - res.Headers.Add(key, value); - } - // Try to prevent compatibility view - res.Headers["Access-Control-Allow-Headers"] = "Accept, Accept-Language, Authorization, Cache-Control, " + - "Content-Disposition, Content-Encoding, Content-Language, Content-Length, Content-MD5, Content-Range, " + - "Content-Type, Cookie, Date, Host, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, " + - "Origin, OriginToken, Pragma, Range, Slug, Transfer-Encoding, Want-Digest, X-MediaBrowser-Token, " + - "X-Emby-Authorization"; - - if (dto is Exception exception) - { - _logger.LogError(exception, "Error processing request for {RawUrl}", req.RawUrl); - - if (!string.IsNullOrEmpty(exception.Message)) - { - var error = exception.Message.Replace(Environment.NewLine, " ", StringComparison.Ordinal); - error = RemoveControlCharacters(error); - - res.Headers.Add("X-Application-Error-Code", error); - } - } - - if (dto is IHasHeaders hasHeaders) - { - if (!hasHeaders.Headers.ContainsKey(HeaderNames.Server)) - { - hasHeaders.Headers[HeaderNames.Server] = "Microsoft-NetCore/2.0, UPnP/1.0 DLNADOC/1.50"; - } - - // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy - if (hasHeaders.Headers.TryGetValue(HeaderNames.ContentLength, out string contentLength) - && !string.IsNullOrEmpty(contentLength)) - { - var length = long.Parse(contentLength, CultureInfo.InvariantCulture); - - if (length > 0) - { - res.ContentLength = length; - } - } - } - } - - /// <summary> - /// Removes the control characters. - /// </summary> - /// <param name="inString">The in string.</param> - /// <returns>System.String.</returns> - public static string RemoveControlCharacters(string inString) - { - if (inString == null) - { - return null; - } - else if (inString.Length == 0) - { - return inString; - } - - var newString = new StringBuilder(inString.Length); - - foreach (var ch in inString) - { - if (!char.IsControl(ch)) - { - newString.Append(ch); - } - } - - return newString.ToString(); - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 76c1d9bac..68d981ad1 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -1,17 +1,7 @@ #pragma warning disable CS1591 -using System; -using System.Linq; -using Emby.Server.Implementations.SocketSharp; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller.Authentication; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; -using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security @@ -19,32 +9,11 @@ namespace Emby.Server.Implementations.HttpServer.Security public class AuthService : IAuthService { private readonly IAuthorizationContext _authorizationContext; - private readonly ISessionManager _sessionManager; - private readonly IServerConfigurationManager _config; - private readonly INetworkManager _networkManager; public AuthService( - IAuthorizationContext authorizationContext, - IServerConfigurationManager config, - ISessionManager sessionManager, - INetworkManager networkManager) + IAuthorizationContext authorizationContext) { _authorizationContext = authorizationContext; - _config = config; - _sessionManager = sessionManager; - _networkManager = networkManager; - } - - public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes) - { - ValidateUser(request, authAttributes); - } - - public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes) - { - var req = new WebSocketSharpRequest(request, null, request.Path); - var user = ValidateUser(req, authAttributes); - return user; } public AuthorizationInfo Authenticate(HttpRequest request) @@ -62,185 +31,5 @@ namespace Emby.Server.Implementations.HttpServer.Security return auth; } - - private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes) - { - // This code is executed before the service - var auth = _authorizationContext.GetAuthorizationInfo(request); - - if (!IsExemptFromAuthenticationToken(authAttributes, request)) - { - ValidateSecurityToken(request, auth.Token); - } - - if (authAttributes.AllowLocalOnly && !request.IsLocal) - { - throw new SecurityException("Operation not found."); - } - - var user = auth.User; - - if (user == null && auth.UserId != Guid.Empty) - { - throw new AuthenticationException("User with Id " + auth.UserId + " not found"); - } - - if (user != null) - { - ValidateUserAccess(user, request, authAttributes); - } - - var info = GetTokenInfo(request); - - if (!IsExemptFromRoles(auth, authAttributes, request, info)) - { - var roles = authAttributes.GetRoles(); - - ValidateRoles(roles, user); - } - - if (!string.IsNullOrEmpty(auth.DeviceId) && - !string.IsNullOrEmpty(auth.Client) && - !string.IsNullOrEmpty(auth.Device)) - { - _sessionManager.LogSessionActivity( - auth.Client, - auth.Version, - auth.DeviceId, - auth.Device, - request.RemoteIp, - user); - } - - return user; - } - - private void ValidateUserAccess( - User user, - IRequest request, - IAuthenticationAttributes authAttributes) - { - if (user.HasPermission(PermissionKind.IsDisabled)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp)) - { - throw new SecurityException("User account has been disabled."); - } - - if (!user.HasPermission(PermissionKind.IsAdministrator) - && !authAttributes.EscapeParentalControl - && !user.IsParentalScheduleAllowed()) - { - request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl"); - - throw new SecurityException("This user account is not allowed access at this time."); - } - } - - private bool IsExemptFromAuthenticationToken(IAuthenticationAttributes authAttribtues, IRequest request) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (authAttribtues.IgnoreLegacyAuth) - { - return true; - } - - return false; - } - - private bool IsExemptFromRoles(AuthorizationInfo auth, IAuthenticationAttributes authAttribtues, IRequest request, AuthenticationInfo tokenInfo) - { - if (!_config.Configuration.IsStartupWizardCompleted && authAttribtues.AllowBeforeStartupWizard) - { - return true; - } - - if (authAttribtues.AllowLocal && request.IsLocal) - { - return true; - } - - if (authAttribtues.AllowLocalOnly && request.IsLocal) - { - return true; - } - - if (string.IsNullOrEmpty(auth.Token)) - { - return true; - } - - if (tokenInfo != null && tokenInfo.UserId.Equals(Guid.Empty)) - { - return true; - } - - return false; - } - - private static void ValidateRoles(string[] roles, User user) - { - if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.IsAdministrator)) - { - throw new SecurityException("User does not have admin access."); - } - } - - if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion)) - { - throw new SecurityException("User does not have delete access."); - } - } - - if (roles.Contains("download", StringComparer.OrdinalIgnoreCase)) - { - if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading)) - { - throw new SecurityException("User does not have download access."); - } - } - } - - private static AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; - } - - private void ValidateSecurityToken(IRequest request, string token) - { - if (string.IsNullOrEmpty(token)) - { - throw new AuthenticationException("Access token is required."); - } - - var info = GetTokenInfo(request); - - if (info == null) - { - throw new AuthenticationException("Access token is invalid or expired."); - } - } } } diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index fb93fae3e..4b407dd9d 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -7,7 +7,6 @@ using System.Net; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; @@ -24,14 +23,9 @@ namespace Emby.Server.Implementations.HttpServer.Security _userManager = userManager; } - public AuthorizationInfo GetAuthorizationInfo(object requestContext) + public AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext) { - return GetAuthorizationInfo((IRequest)requestContext); - } - - public AuthorizationInfo GetAuthorizationInfo(IRequest requestContext) - { - if (requestContext.Items.TryGetValue("AuthorizationInfo", out var cached)) + if (requestContext.Request.HttpContext.Items.TryGetValue("AuthorizationInfo", out var cached)) { return (AuthorizationInfo)cached; } @@ -52,18 +46,18 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private AuthorizationInfo GetAuthorization(IRequest httpReq) + private AuthorizationInfo GetAuthorization(HttpContext httpReq) { var auth = GetAuthorizationDictionary(httpReq); var (authInfo, originalAuthInfo) = - GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString); + GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query); if (originalAuthInfo != null) { - httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo; + httpReq.Request.HttpContext.Items["OriginalAuthenticationInfo"] = originalAuthInfo; } - httpReq.Items["AuthorizationInfo"] = authInfo; + httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; return authInfo; } @@ -203,13 +197,13 @@ namespace Emby.Server.Implementations.HttpServer.Security /// </summary> /// <param name="httpReq">The HTTP req.</param> /// <returns>Dictionary{System.StringSystem.String}.</returns> - private Dictionary<string, string> GetAuthorizationDictionary(IRequest httpReq) + private Dictionary<string, string> GetAuthorizationDictionary(HttpContext httpReq) { - var auth = httpReq.Headers["X-Emby-Authorization"]; + var auth = httpReq.Request.Headers["X-Emby-Authorization"]; if (string.IsNullOrEmpty(auth)) { - auth = httpReq.Headers[HeaderNames.Authorization]; + auth = httpReq.Request.Headers[HeaderNames.Authorization]; } return GetAuthorization(auth); diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index 03fcfa53d..8777c59b7 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -2,11 +2,11 @@ using System; using Jellyfin.Data.Entities; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; -using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace Emby.Server.Implementations.HttpServer.Security { @@ -23,26 +23,20 @@ namespace Emby.Server.Implementations.HttpServer.Security _sessionManager = sessionManager; } - public SessionInfo GetSession(IRequest requestContext) + public SessionInfo GetSession(HttpContext requestContext) { var authorization = _authContext.GetAuthorizationInfo(requestContext); var user = authorization.User; - return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.RemoteIp, user); - } - - private AuthenticationInfo GetTokenInfo(IRequest request) - { - request.Items.TryGetValue("OriginalAuthenticationInfo", out var info); - return info as AuthenticationInfo; + return _sessionManager.LogSessionActivity(authorization.Client, authorization.Version, authorization.DeviceId, authorization.Device, requestContext.Request.RemoteIp(), user); } public SessionInfo GetSession(object requestContext) { - return GetSession((IRequest)requestContext); + return GetSession((HttpContext)requestContext); } - public User GetUser(IRequest requestContext) + public User GetUser(HttpContext requestContext) { var session = GetSession(requestContext); @@ -51,7 +45,7 @@ namespace Emby.Server.Implementations.HttpServer.Security public User GetUser(object requestContext) { - return GetUser((IRequest)requestContext); + return GetUser((HttpContext)requestContext); } } } diff --git a/Emby.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs deleted file mode 100644 index 00e3ab8fe..000000000 --- a/Emby.Server.Implementations/HttpServer/StreamWriter.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; -using Microsoft.Net.Http.Headers; - -namespace Emby.Server.Implementations.HttpServer -{ - /// <summary> - /// Class StreamWriter. - /// </summary> - public class StreamWriter : IAsyncStreamWriter, IHasHeaders - { - /// <summary> - /// The options. - /// </summary> - private readonly IDictionary<string, string> _options = new Dictionary<string, string>(); - - /// <summary> - /// Initializes a new instance of the <see cref="StreamWriter" /> class. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - public StreamWriter(Stream source, string contentType) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceStream = source; - - Headers["Content-Type"] = contentType; - - if (source.CanSeek) - { - Headers[HeaderNames.ContentLength] = source.Length.ToString(CultureInfo.InvariantCulture); - } - - Headers[HeaderNames.ContentType] = contentType; - } - - /// <summary> - /// Initializes a new instance of the <see cref="StreamWriter"/> class. - /// </summary> - /// <param name="source">The source.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="contentLength">The content length.</param> - public StreamWriter(byte[] source, string contentType, int contentLength) - { - if (string.IsNullOrEmpty(contentType)) - { - throw new ArgumentNullException(nameof(contentType)); - } - - SourceBytes = source; - - Headers[HeaderNames.ContentLength] = contentLength.ToString(CultureInfo.InvariantCulture); - Headers[HeaderNames.ContentType] = contentType; - } - - /// <summary> - /// Gets or sets the source stream. - /// </summary> - /// <value>The source stream.</value> - private Stream SourceStream { get; set; } - - private byte[] SourceBytes { get; set; } - - /// <summary> - /// Gets the options. - /// </summary> - /// <value>The options.</value> - public IDictionary<string, string> Headers => _options; - - /// <summary> - /// Fires when complete. - /// </summary> - public Action OnComplete { get; set; } - - /// <summary> - /// Fires when an error occours. - /// </summary> - public Action OnError { get; set; } - - /// <inheritdoc /> - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - try - { - var bytes = SourceBytes; - - if (bytes != null) - { - await responseStream.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); - } - else - { - using (var src = SourceStream) - { - await src.CopyToAsync(responseStream, cancellationToken).ConfigureAwait(false); - } - } - } - catch - { - OnError?.Invoke(); - - throw; - } - finally - { - OnComplete?.Invoke(); - } - } - } -} diff --git a/Emby.Server.Implementations/HttpServer/WebSocketManager.cs b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs new file mode 100644 index 000000000..89c1b7ea0 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/WebSocketManager.cs @@ -0,0 +1,102 @@ +#pragma warning disable CS1591 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.HttpServer +{ + public class WebSocketManager : IWebSocketManager + { + private readonly ILogger<WebSocketManager> _logger; + private readonly ILoggerFactory _loggerFactory; + + private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>(); + private bool _disposed = false; + + public WebSocketManager( + ILogger<WebSocketManager> logger, + ILoggerFactory loggerFactory) + { + _logger = logger; + _loggerFactory = loggerFactory; + } + + public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; + + /// <inheritdoc /> + public async Task WebSocketRequestHandler(HttpContext context) + { + if (_disposed) + { + return; + } + + try + { + _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress); + + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false); + + using var connection = new WebSocketConnection( + _loggerFactory.CreateLogger<WebSocketConnection>(), + webSocket, + context.Connection.RemoteIpAddress, + context.Request.Query) + { + OnReceive = ProcessWebSocketMessageReceived + }; + + WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection)); + + await connection.ProcessAsync().ConfigureAwait(false); + _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress); + } + catch (Exception ex) // Otherwise ASP.Net will ignore the exception + { + _logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress); + if (!context.Response.HasStarted) + { + context.Response.StatusCode = 500; + } + } + } + + /// <summary> + /// Adds the rest handlers. + /// </summary> + /// <param name="listeners">The web socket listeners.</param> + public void Init(IEnumerable<IWebSocketListener> listeners) + { + _webSocketListeners = listeners.ToArray(); + } + + /// <summary> + /// Processes the web socket message received. + /// </summary> + /// <param name="result">The result.</param> + private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result) + { + if (_disposed) + { + return Task.CompletedTask; + } + + IEnumerable<Task> GetTasks() + { + foreach (var x in _webSocketListeners) + { + yield return x.ProcessMessageAsync(result); + } + } + + return Task.WhenAll(GetTasks()); + } + } +} diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index fe74f1de7..7435e9d0b 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -149,7 +149,7 @@ namespace Emby.Server.Implementations.IO continue; } - _logger.LogInformation("{name} ({path}) will be refreshed.", item.Name, item.Path); + _logger.LogInformation("{Name} ({Path}) will be refreshed.", item.Name, item.Path); try { @@ -160,11 +160,11 @@ namespace Emby.Server.Implementations.IO // For now swallow and log. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) // Should we remove it from it's parent? - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } catch (Exception ex) { - _logger.LogError(ex, "Error refreshing {name}", item.Name); + _logger.LogError(ex, "Error refreshing {Name}", item.Name); } } } @@ -214,6 +214,7 @@ namespace Emby.Server.Implementations.IO } } + /// <inheritdoc /> public void Dispose() { _disposed = true; diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index 9290dfcd0..3353fae9d 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -88,7 +88,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error in ReportFileSystemChanged for {path}", path); + _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index ab6483bf9..3cb025111 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -398,30 +398,6 @@ namespace Emby.Server.Implementations.IO } } - public virtual void SetReadOnly(string path, bool isReadOnly) - { - if (OperatingSystem.Id != OperatingSystemId.Windows) - { - return; - } - - var info = GetExtendedFileSystemInfo(path); - - if (info.Exists && info.IsReadOnly != isReadOnly) - { - if (isReadOnly) - { - File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); - } - else - { - var attributes = File.GetAttributes(path); - attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); - File.SetAttributes(path, attributes); - } - } - } - public virtual void SetAttributes(string path, bool isHidden, bool isReadOnly) { if (OperatingSystem.Id != OperatingSystemId.Windows) @@ -707,14 +683,6 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } - public virtual void SetExecutable(string path) - { - if (OperatingSystem.Id == OperatingSystemId.Darwin) - { - RunProcess("chmod", "+x \"" + path + "\"", Path.GetDirectoryName(path)); - } - } - private static void RunProcess(string path, string args, string workingDirectory) { using (var process = Process.Start(new ProcessStartInfo diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/Emby.Server.Implementations/IO/StreamHelper.cs index 40b397edc..c16ebd61b 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/Emby.Server.Implementations/IO/StreamHelper.cs @@ -11,8 +11,6 @@ namespace Emby.Server.Implementations.IO { public class StreamHelper : IStreamHelper { - private const int StreamCopyToBufferSize = 81920; - public async Task CopyToAsync(Stream source, Stream destination, int bufferSize, Action onStarted, CancellationToken cancellationToken) { byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); @@ -83,37 +81,9 @@ namespace Emby.Server.Implementations.IO } } - public async Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); - try - { - int totalBytesRead = 0; - - int bytesRead; - while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - totalBytesRead += bytesRead; - } - } - - return totalBytesRead; - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) { - byte[] buffer = ArrayPool<byte>.Shared.Rent(StreamCopyToBufferSize); + byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize); try { int bytesRead; diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 2e13a3bb3..0edd98031 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -52,10 +52,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await directStreamProvider.CopyToAsync(output, cancellationToken).ConfigureAwait(false); + await directStreamProvider.CopyToAsync(output, cancellationTokenSource.Token).ConfigureAwait(false); } _logger.LogInformation("Recording completed to file {0}", targetFile); @@ -72,7 +72,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV UserAgent = "Emby/3.0", // Shouldn't matter but may cause issues - DecompressionMethod = CompressionMethods.None + DecompressionMethod = CompressionMethods.None, + CancellationToken = cancellationToken }; using (var response = await _httpClient.SendAsync(httpRequestOptions, HttpMethod.Get).ConfigureAwait(false)) @@ -88,10 +89,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogInformation("Copying recording stream to file {0}", targetFile); // The media source if infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; - - await _streamHelper.CopyUntilCancelled(response.Content, output, 81920, cancellationToken).ConfigureAwait(false); + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); + + await _streamHelper.CopyUntilCancelled( + response.Content, + output, + IODefaults.CopyToBufferSize, + cancellationTokenSource.Token).ConfigureAwait(false); } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 09c52d95b..5cf09b8e5 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -604,11 +604,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return Task.CompletedTask; } - public Task DeleteRecordingAsync(string recordingId, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task CreateSeriesTimerAsync(SeriesTimerInfo info, CancellationToken cancellationToken) { throw new NotImplementedException(); @@ -808,11 +803,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return null; } - public IEnumerable<ActiveRecordingInfo> GetAllActiveRecordings() - { - return _activeRecordings.Values.Where(i => i.Timer.Status == RecordingStatus.InProgress && !i.CancellationTokenSource.IsCancellationRequested); - } - public ActiveRecordingInfo GetActiveRecordingInfo(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -1015,16 +1005,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new Exception("Tuner not found."); } - private MediaSourceInfo CloneMediaSource(MediaSourceInfo mediaSource, bool enableStreamSharing) - { - var json = _jsonSerializer.SerializeToString(mediaSource); - mediaSource = _jsonSerializer.DeserializeFromString<MediaSourceInfo>(json); - - mediaSource.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture) + "_" + mediaSource.Id; - - return mediaSource; - } - public async Task<List<MediaSourceInfo>> GetChannelStreamMediaSources(string channelId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(channelId)) @@ -1654,7 +1634,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (mediaSource.RequiresLooping || !(mediaSource.Container ?? string.Empty).EndsWith("ts", StringComparison.OrdinalIgnoreCase) || (mediaSource.Protocol != MediaProtocol.File && mediaSource.Protocol != MediaProtocol.Http)) { - return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer, _config); + return new EncodedRecorder(_logger, _mediaEncoder, _config.ApplicationPaths, _jsonSerializer); } return new DirectRecorder(_logger, _httpClient, _streamHelper); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 612dc5238..3e5457dbd 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -8,12 +8,9 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; -using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; @@ -26,26 +23,24 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; + private readonly IJsonSerializer _json; + private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); + private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; - private readonly IJsonSerializer _json; - private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); - private readonly IServerConfigurationManager _config; public EncodedRecorder( ILogger logger, IMediaEncoder mediaEncoder, IServerApplicationPaths appPaths, - IJsonSerializer json, - IServerConfigurationManager config) + IJsonSerializer json) { _logger = logger; _mediaEncoder = mediaEncoder; _appPaths = appPaths; _json = json; - _config = config; } private static bool CopySubtitles => false; @@ -58,19 +53,14 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public async Task Record(IDirectStreamProvider directStreamProvider, MediaSourceInfo mediaSource, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { // The media source is infinite so we need to handle stopping ourselves - var durationToken = new CancellationTokenSource(duration); - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token).Token; + using var durationToken = new CancellationTokenSource(duration); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationToken).ConfigureAwait(false); + await RecordFromFile(mediaSource, mediaSource.Path, targetFile, duration, onStarted, cancellationTokenSource.Token).ConfigureAwait(false); _logger.LogInformation("Recording completed to file {0}", targetFile); } - private EncodingOptions GetEncodingOptions() - { - return _config.GetConfiguration<EncodingOptions>("encoding"); - } - private Task RecordFromFile(MediaSourceInfo mediaSource, string inputFile, string targetFile, TimeSpan duration, Action onStarted, CancellationToken cancellationToken) { _targetPath = targetFile; @@ -108,7 +98,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV StartInfo = processStartInfo, EnableRaisingEvents = true }; - _process.Exited += (sender, args) => OnFfMpegProcessExited(_process, inputFile); + _process.Exited += (sender, args) => OnFfMpegProcessExited(_process); _process.Start(); @@ -221,20 +211,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } protected string GetOutputSizeParam() - { - var filters = new List<string>(); - - filters.Add("yadif=0:-1:0"); - - var output = string.Empty; - - if (filters.Count > 0) - { - output += string.Format(CultureInfo.InvariantCulture, " -vf \"{0}\"", string.Join(",", filters.ToArray())); - } - - return output; - } + => "-vf \"yadif=0:-1:0\""; private void Stop() { @@ -291,7 +268,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <summary> /// Processes the exited. /// </summary> - private void OnFfMpegProcessExited(Process process, string inputFile) + private void OnFfMpegProcessExited(Process process) { using (process) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index c4d5cc58a..33331adaf 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -24,14 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings { public class SchedulesDirect : IListingsProvider { + private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; + private readonly ILogger<SchedulesDirect> _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IHttpClient _httpClient; private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1); private readonly IApplicationHost _appHost; - private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; - public SchedulesDirect( ILogger<SchedulesDirect> logger, IJsonSerializer jsonSerializer, @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings while (start <= end) { - dates.Add(start.ToString("yyyy-MM-dd")); + dates.Add(start.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); start = start.AddDays(1); } @@ -367,13 +367,14 @@ namespace Emby.Server.Implementations.LiveTv.Listings if (!string.IsNullOrWhiteSpace(details.originalAirDate)) { - info.OriginalAirDate = DateTime.Parse(details.originalAirDate); + info.OriginalAirDate = DateTime.Parse(details.originalAirDate, CultureInfo.InvariantCulture); info.ProductionYear = info.OriginalAirDate.Value.Year; } if (details.movie != null) { - if (!string.IsNullOrEmpty(details.movie.year) && int.TryParse(details.movie.year, out int year)) + if (!string.IsNullOrEmpty(details.movie.year) + && int.TryParse(details.movie.year, out int year)) { info.ProductionYear = year; } @@ -587,7 +588,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - NameValuePair savedToken = null; + NameValuePair savedToken; if (!_tokens.TryGetValue(username, out savedToken)) { savedToken = new NameValuePair(); @@ -633,7 +634,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings } } - private async Task<HttpResponseInfo> Post(HttpRequestOptions options, + private async Task<HttpResponseInfo> Post( + HttpRequestOptions options, bool enableRetry, ListingsProviderInfo providerInfo) { @@ -663,7 +665,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings return await Post(options, false, providerInfo).ConfigureAwait(false); } - private async Task<HttpResponseInfo> Get(HttpRequestOptions options, + private async Task<HttpResponseInfo> Get( + HttpRequestOptions options, bool enableRetry, ListingsProviderInfo providerInfo) { @@ -693,7 +696,9 @@ namespace Emby.Server.Implementations.LiveTv.Listings return await Get(options, false, providerInfo).ConfigureAwait(false); } - private async Task<string> GetTokenInternal(string username, string password, + private async Task<string> GetTokenInternal( + string username, + string password, CancellationToken cancellationToken) { var httpOptions = new HttpRequestOptions() diff --git a/Emby.Server.Implementations/Localization/Core/es_DO.json b/Emby.Server.Implementations/Localization/Core/es_DO.json index 0ef16542f..26732eb3f 100644 --- a/Emby.Server.Implementations/Localization/Core/es_DO.json +++ b/Emby.Server.Implementations/Localization/Core/es_DO.json @@ -17,5 +17,8 @@ "Genres": "Géneros", "Folders": "Carpetas", "Favorites": "Favoritos", - "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}" + "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido de {0}", + "HeaderFavoriteSongs": "Canciones Favoritas", + "HeaderFavoriteEpisodes": "Episodios Favoritos", + "HeaderFavoriteArtists": "Artistas Favoritos" } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 1b55c2e38..a97c2e17a 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -45,7 +45,7 @@ "NameSeasonNumber": "Sesong {0}", "NameSeasonUnknown": "Sesong ukjent", "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", - "NotificationOptionApplicationUpdateAvailable": "Programvareoppdatering er tilgjengelig", + "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", @@ -71,7 +71,7 @@ "ScheduledTaskFailedWithName": "{0} mislykkes", "ScheduledTaskStartedWithName": "{0} startet", "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", - "Shows": "Programmer", + "Shows": "Program", "Songs": "Sanger", "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", @@ -88,7 +88,7 @@ "UserOnlineFromDevice": "{0} er tilkoblet fra {1}", "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", "UserPolicyUpdatedWithName": "Brukerpolicyen har blitt oppdatert for {0}", - "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1}", + "UserStartedPlayingItemWithValues": "{0} har startet avspilling {1} på {2}", "UserStoppedPlayingItemWithValues": "{0} har stoppet avspilling {1}", "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", "ValueSpecialEpisodeName": "Spesialepisode - {0}", diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 281cadac5..fb6e81beb 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -35,7 +35,7 @@ "AuthenticationSucceededWithUserName": "{0} Har logga inn", "Artists": "Artistar", "Application": "Program", - "AppDeviceValues": "App: {0}, Einheit: {1}", + "AppDeviceValues": "App: {0}, Eining: {1}", "Albums": "Album", "NotificationOptionServerRestartRequired": "Tenaren krev omstart", "NotificationOptionPluginUpdateInstalled": "Tilleggsprogram-oppdatering vart installert", @@ -43,7 +43,7 @@ "NotificationOptionPluginInstalled": "Tilleggsprogram installert", "NotificationOptionPluginError": "Tilleggsprogram feila", "NotificationOptionNewLibraryContent": "Nytt innhald er lagt til", - "NotificationOptionInstallationFailed": "Installasjonen feila", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionCameraImageUploaded": "Kamerabilde vart lasta opp", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppa", "NotificationOptionAudioPlayback": "Lydavspilling påbyrja", @@ -56,5 +56,62 @@ "MusicVideos": "Musikkvideoar", "Music": "Musikk", "Movies": "Filmar", - "MixedContent": "Blanda innhald" + "MixedContent": "Blanda innhald", + "Sync": "Synkronisera", + "TaskDownloadMissingSubtitlesDescription": "Søk Internettet for manglande undertekstar basert på metadatainnstillingar.", + "TaskDownloadMissingSubtitles": "Last ned manglande undertekstar", + "TaskRefreshChannelsDescription": "Oppdater internettkanalinformasjon.", + "TaskRefreshChannels": "Oppdater kanalar", + "TaskCleanTranscodeDescription": "Slett transkodefiler som er meir enn ein dag gamal.", + "TaskCleanTranscode": "Reins transkodemappe", + "TaskUpdatePluginsDescription": "Laster ned og installerer oppdateringar for programtillegg som er sette opp til å oppdaterast automatisk.", + "TaskUpdatePlugins": "Oppdaterer programtillegg", + "TaskRefreshPeopleDescription": "Oppdaterer metadata for skodespelarar og regissørar i mediebiblioteket ditt.", + "TaskRefreshPeople": "Oppdater personar", + "TaskCleanLogsDescription": "Slett loggfiler som er meir enn {0} dagar gamle.", + "TaskCleanLogs": "Reins loggmappe", + "TaskRefreshLibraryDescription": "Skannar mediebiblioteket ditt for nye filer og oppdaterer metadata.", + "TaskRefreshLibrary": "Skann mediebibliotek", + "TaskRefreshChapterImagesDescription": "Lager miniatyrbilete for videoar som har kapittel.", + "TaskRefreshChapterImages": "Trekk ut kapittelbilete", + "TaskCleanCacheDescription": "Slettar mellomlagra filer som ikkje lengre trengst av systemet.", + "TaskCleanCache": "Rens mappe for hurtiglager", + "TasksChannelsCategory": "Internettkanalar", + "TasksApplicationCategory": "Applikasjon", + "TasksLibraryCategory": "Bibliotek", + "TasksMaintenanceCategory": "Vedlikehald", + "VersionNumber": "Versjon {0}", + "ValueSpecialEpisodeName": "Spesialepisode - {0}", + "ValueHasBeenAddedToLibrary": "{0} har blitt lagt til i mediebiblioteket ditt", + "UserStoppedPlayingItemWithValues": "{0} har fullført avspeling {1} på {2}", + "UserStartedPlayingItemWithValues": "{0} spelar {1} på {2}", + "UserPolicyUpdatedWithName": "Brukarreglar har blitt oppdatert for {0}", + "UserPasswordChangedWithName": "Passordet for {0} er oppdatert", + "UserOnlineFromDevice": "{0} er direktekopla frå {1}", + "UserOfflineFromDevice": "{0} har kopla frå {1}", + "UserLockedOutWithName": "Brukar {0} har blitt utestengd", + "UserDownloadingItemWithValues": "{0} lastar ned {1}", + "UserDeletedWithName": "Brukar {0} er sletta", + "UserCreatedWithName": "Brukar {0} er oppretta", + "User": "Brukar", + "TvShows": "TV-seriar", + "System": "System", + "SubtitleDownloadFailureFromForItem": "Feila å laste ned undertekstar frå {0} for {1}", + "StartupEmbyServerIsLoading": "Jellyfintenaren laster. Prøv igjen om litt.", + "Songs": "Songar", + "Shows": "Program", + "ServerNameNeedsToBeRestarted": "{0} må omstartast", + "ScheduledTaskStartedWithName": "{0} starta", + "ScheduledTaskFailedWithName": "{0} feila", + "ProviderValue": "Leverandør: {0}", + "PluginUpdatedWithName": "{0} blei oppdatert", + "PluginUninstalledWithName": "{0} blei avinstallert", + "PluginInstalledWithName": "{0} blei installert", + "Plugin": "Programtillegg", + "Playlists": "Speleliste", + "Photos": "Foto", + "NotificationOptionVideoPlaybackStopped": "Videoavspeling stoppa", + "NotificationOptionVideoPlayback": "Videoavspeling starta", + "NotificationOptionUserLockedOut": "Brukar er utestengd", + "NotificationOptionTaskFailed": "Planlagt oppgåve feila" } diff --git a/Emby.Server.Implementations/Localization/Core/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 42500670d..3f6f3b23c 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -1,76 +1,117 @@ { "ProviderValue": "ผู้ให้บริการ: {0}", - "PluginUpdatedWithName": "{0} ได้รับการ update แล้ว", - "PluginUninstalledWithName": "ถอนการติดตั้ง {0}", - "PluginInstalledWithName": "{0} ได้รับการติดตั้ง", - "Plugin": "Plugin", - "Playlists": "รายการ", + "PluginUpdatedWithName": "อัปเดต {0} แล้ว", + "PluginUninstalledWithName": "ถอนการติดตั้ง {0} แล้ว", + "PluginInstalledWithName": "ติดตั้ง {0} แล้ว", + "Plugin": "ปลั๊กอิน", + "Playlists": "เพลย์ลิสต์", "Photos": "รูปภาพ", - "NotificationOptionVideoPlaybackStopped": "หยุดการเล่น Video", - "NotificationOptionVideoPlayback": "เริ่มแสดง Video", - "NotificationOptionUserLockedOut": "ผู้ใช้ Locked Out", - "NotificationOptionTaskFailed": "ตารางการทำงานล้มเหลว", - "NotificationOptionServerRestartRequired": "ควร Restart Server", - "NotificationOptionPluginUpdateInstalled": "Update Plugin แล้ว", - "NotificationOptionPluginUninstalled": "ถอด Plugin", - "NotificationOptionPluginInstalled": "ติดตั้ง Plugin แล้ว", - "NotificationOptionPluginError": "Plugin ล้มเหลว", - "NotificationOptionNewLibraryContent": "เพิ่มข้อมูลใหม่แล้ว", - "NotificationOptionInstallationFailed": "ติดตั้งล้มเหลว", - "NotificationOptionCameraImageUploaded": "รูปภาพถูก upload", - "NotificationOptionAudioPlaybackStopped": "หยุดการเล่นเสียง", + "NotificationOptionVideoPlaybackStopped": "หยุดเล่นวิดีโอ", + "NotificationOptionVideoPlayback": "เริ่มเล่นวิดีโอ", + "NotificationOptionUserLockedOut": "ผู้ใช้ถูกล็อก", + "NotificationOptionTaskFailed": "งานตามกำหนดการล้มเหลว", + "NotificationOptionServerRestartRequired": "จำเป็นต้องรีสตาร์ทเซิร์ฟเวอร์", + "NotificationOptionPluginUpdateInstalled": "ติดตั้งการอัปเดตปลั๊กอินแล้ว", + "NotificationOptionPluginUninstalled": "ถอนการติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginInstalled": "ติดตั้งปลั๊กอินแล้ว", + "NotificationOptionPluginError": "ปลั๊กอินล้มเหลว", + "NotificationOptionNewLibraryContent": "เพิ่มเนื้อหาใหม่แล้ว", + "NotificationOptionInstallationFailed": "การติดตั้งล้มเหลว", + "NotificationOptionCameraImageUploaded": "อัปโหลดภาพถ่ายแล้ว", + "NotificationOptionAudioPlaybackStopped": "หยุดเล่นเสียง", "NotificationOptionAudioPlayback": "เริ่มเล่นเสียง", - "NotificationOptionApplicationUpdateInstalled": "Update ระบบแล้ว", - "NotificationOptionApplicationUpdateAvailable": "ระบบ update สามารถใช้ได้แล้ว", - "NewVersionIsAvailable": "ตรวจพบ Jellyfin เวอร์ชั่นใหม่", - "NameSeasonUnknown": "ไม่ทราบปี", - "NameSeasonNumber": "ปี {0}", - "NameInstallFailed": "{0} ติดตั้งไม่สำเร็จ", - "MusicVideos": "MV", - "Music": "เพลง", - "Movies": "ภาพยนต์", - "MixedContent": "รายการแบบผสม", - "MessageServerConfigurationUpdated": "การตั้งค่า update แล้ว", - "MessageNamedServerConfigurationUpdatedWithValue": "รายการตั้งค่า {0} ได้รับการ update แล้ว", - "MessageApplicationUpdatedTo": "Jellyfin Server จะ update ไปที่ {0}", - "MessageApplicationUpdated": "Jellyfin Server update แล้ว", + "NotificationOptionApplicationUpdateInstalled": "ติดตั้งการอัปเดตแอพพลิเคชันแล้ว", + "NotificationOptionApplicationUpdateAvailable": "มีการอัปเดตแอพพลิเคชัน", + "NewVersionIsAvailable": "เวอร์ชันใหม่ของเซิร์ฟเวอร์ Jellyfin พร้อมให้ดาวน์โหลดแล้ว", + "NameSeasonUnknown": "ไม่ทราบซีซัน", + "NameSeasonNumber": "ซีซัน {0}", + "NameInstallFailed": "การติดตั้ง {0} ล้มเหลว", + "MusicVideos": "มิวสิควิดีโอ", + "Music": "ดนตรี", + "Movies": "ภาพยนตร์", + "MixedContent": "เนื้อหาผสม", + "MessageServerConfigurationUpdated": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์แล้ว", + "MessageNamedServerConfigurationUpdatedWithValue": "อัปเดตการกำหนดค่าเซิร์ฟเวอร์ในส่วน {0} แล้ว", + "MessageApplicationUpdatedTo": "เซิร์ฟเวอร์ Jellyfin ได้รับการอัปเดตเป็น {0}", + "MessageApplicationUpdated": "อัพเดตเซิร์ฟเวอร์ Jellyfin แล้ว", "Latest": "ล่าสุด", - "LabelRunningTimeValue": "เวลาที่เล่น : {0}", - "LabelIpAddressValue": "IP address: {0}", - "ItemRemovedWithName": "{0} ถูกลบจากรายการ", - "ItemAddedWithName": "{0} ถูกเพิ่มในรายการ", - "Inherit": "การสืบทอด", - "HomeVideos": "วีดีโอส่วนตัว", - "HeaderRecordingGroups": "ค่ายบันทึก", + "LabelRunningTimeValue": "ผ่านไปแล้ว: {0}", + "LabelIpAddressValue": "ที่อยู่ IP: {0}", + "ItemRemovedWithName": "{0} ถูกลบออกจากไลบรารี", + "ItemAddedWithName": "{0} ถูกเพิ่มลงในไลบรารีแล้ว", + "Inherit": "สืบทอด", + "HomeVideos": "โฮมวิดีโอ", + "HeaderRecordingGroups": "กลุ่มการบันทึก", "HeaderNextUp": "ถัดไป", - "HeaderLiveTV": "รายการสด", - "HeaderFavoriteSongs": "เพลงโปรด", - "HeaderFavoriteShows": "รายการโชว์โปรด", - "HeaderFavoriteEpisodes": "ฉากโปรด", - "HeaderFavoriteArtists": "นักแสดงโปรด", - "HeaderFavoriteAlbums": "อัมบั้มโปรด", - "HeaderContinueWatching": "ชมต่อจากเดิม", - "HeaderCameraUploads": "Upload รูปภาพ", - "HeaderAlbumArtists": "อัลบั้มนักแสดง", + "HeaderLiveTV": "ทีวีสด", + "HeaderFavoriteSongs": "เพลงที่ชื่นชอบ", + "HeaderFavoriteShows": "รายการที่ชื่นชอบ", + "HeaderFavoriteEpisodes": "ตอนที่ชื่นชอบ", + "HeaderFavoriteArtists": "ศิลปินที่ชื่นชอบ", + "HeaderFavoriteAlbums": "อัมบั้มที่ชื่นชอบ", + "HeaderContinueWatching": "ดูต่อ", + "HeaderCameraUploads": "อัปโหลดรูปถ่าย", + "HeaderAlbumArtists": "อัลบั้มศิลปิน", "Genres": "ประเภท", "Folders": "โฟลเดอร์", "Favorites": "รายการโปรด", - "FailedLoginAttemptWithUserName": "การเชื่อมต่อล้มเหลวจาก {0}", - "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จ", - "DeviceOfflineWithName": "{0} ตัดการเชื่อมต่อ", - "Collections": "ชุด", - "ChapterNameValue": "บทที่ {0}", - "Channels": "ชาแนล", - "CameraImageUploadedFrom": "รูปภาพถูก upload จาก {0}", + "FailedLoginAttemptWithUserName": "ความพยายามในการเข้าสู่ระบบล้มเหลวจาก {0}", + "DeviceOnlineWithName": "{0} เชื่อมต่อสำเร็จแล้ว", + "DeviceOfflineWithName": "{0} ยกเลิกการเชื่อมต่อแล้ว", + "Collections": "คอลเลกชัน", + "ChapterNameValue": "บท {0}", + "Channels": "ช่อง", + "CameraImageUploadedFrom": "ภาพถ่ายใหม่ได้ถูกอัปโหลดมาจาก {0}", "Books": "หนังสือ", - "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จ", - "Artists": "นักแสดง", - "Application": "แอปพลิเคชั่น", - "AppDeviceValues": "App: {0}, อุปกรณ์: {1}", + "AuthenticationSucceededWithUserName": "{0} ยืนยันตัวสำเร็จแล้ว", + "Artists": "ศิลปิน", + "Application": "แอพพลิเคชัน", + "AppDeviceValues": "แอพ: {0}, อุปกรณ์: {1}", "Albums": "อัลบั้ม", "ScheduledTaskStartedWithName": "{0} เริ่มต้น", "ScheduledTaskFailedWithName": "{0} ล้มเหลว", "Songs": "เพลง", - "Shows": "แสดง", - "ServerNameNeedsToBeRestarted": "{0} ต้องการรีสตาร์ท" + "Shows": "รายการ", + "ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท", + "TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา", + "TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป", + "TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต", + "TaskRefreshChannels": "รีเฟรชช่อง", + "TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน", + "TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด", + "TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ", + "TaskUpdatePlugins": "อัปเดตปลั๊กอิน", + "TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ", + "TaskRefreshPeople": "รีเฟรชบุคคล", + "TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน", + "TaskCleanLogs": "ล้างไดเรกทอรีบันทึก", + "TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา", + "TaskRefreshLibrary": "สแกนไลบรารีสื่อ", + "TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท", + "TaskRefreshChapterImages": "แตกรูปภาพบท", + "TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ", + "TaskCleanCache": "ล้างไดเรกทอรีแคช", + "TasksChannelsCategory": "ช่องอินเทอร์เน็ต", + "TasksApplicationCategory": "แอพพลิเคชัน", + "TasksLibraryCategory": "ไลบรารี", + "TasksMaintenanceCategory": "ปิดซ่อมบำรุง", + "VersionNumber": "เวอร์ชัน {0}", + "ValueSpecialEpisodeName": "พิเศษ - {0}", + "ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว", + "UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}", + "UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}", + "UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}", + "UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}", + "UserOnlineFromDevice": "{0} ออนไลน์จาก {1}", + "UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}", + "UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก", + "UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}", + "UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว", + "UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว", + "User": "ผู้ใช้งาน", + "TvShows": "รายการทีวี", + "System": "ระบบ", + "Sync": "ซิงค์", + "SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้", + "StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่" } diff --git a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs new file mode 100644 index 000000000..140a67541 --- /dev/null +++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs @@ -0,0 +1,285 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using MediaBrowser.Common; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Controller.Security; +using MediaBrowser.Model.QuickConnect; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.QuickConnect +{ + /// <summary> + /// Quick connect implementation. + /// </summary> + public class QuickConnectManager : IQuickConnect, IDisposable + { + private readonly RNGCryptoServiceProvider _rng = new RNGCryptoServiceProvider(); + private readonly ConcurrentDictionary<string, QuickConnectResult> _currentRequests = new ConcurrentDictionary<string, QuickConnectResult>(); + + private readonly IServerConfigurationManager _config; + private readonly ILogger<QuickConnectManager> _logger; + private readonly IAuthenticationRepository _authenticationRepository; + private readonly IAuthorizationContext _authContext; + private readonly IServerApplicationHost _appHost; + + /// <summary> + /// Initializes a new instance of the <see cref="QuickConnectManager"/> class. + /// Should only be called at server startup when a singleton is created. + /// </summary> + /// <param name="config">Configuration.</param> + /// <param name="logger">Logger.</param> + /// <param name="appHost">Application host.</param> + /// <param name="authContext">Authentication context.</param> + /// <param name="authenticationRepository">Authentication repository.</param> + public QuickConnectManager( + IServerConfigurationManager config, + ILogger<QuickConnectManager> logger, + IServerApplicationHost appHost, + IAuthorizationContext authContext, + IAuthenticationRepository authenticationRepository) + { + _config = config; + _logger = logger; + _appHost = appHost; + _authContext = authContext; + _authenticationRepository = authenticationRepository; + + ReloadConfiguration(); + } + + /// <inheritdoc/> + public int CodeLength { get; set; } = 6; + + /// <inheritdoc/> + public string TokenName { get; set; } = "QuickConnect"; + + /// <inheritdoc/> + public QuickConnectState State { get; private set; } = QuickConnectState.Unavailable; + + /// <inheritdoc/> + public int Timeout { get; set; } = 5; + + private DateTime DateActivated { get; set; } + + /// <inheritdoc/> + public void AssertActive() + { + if (State != QuickConnectState.Active) + { + throw new ArgumentException("Quick connect is not active on this server"); + } + } + + /// <inheritdoc/> + public void Activate() + { + DateActivated = DateTime.UtcNow; + SetState(QuickConnectState.Active); + } + + /// <inheritdoc/> + public void SetState(QuickConnectState newState) + { + _logger.LogDebug("Changed quick connect state from {State} to {newState}", State, newState); + + ExpireRequests(true); + + State = newState; + _config.Configuration.QuickConnectAvailable = newState == QuickConnectState.Available || newState == QuickConnectState.Active; + _config.SaveConfiguration(); + + _logger.LogDebug("Configuration saved"); + } + + /// <inheritdoc/> + public QuickConnectResult TryConnect() + { + ExpireRequests(); + + if (State != QuickConnectState.Active) + { + _logger.LogDebug("Refusing quick connect initiation request, current state is {State}", State); + throw new AuthenticationException("Quick connect is not active on this server"); + } + + var code = GenerateCode(); + var result = new QuickConnectResult() + { + Secret = GenerateSecureRandom(), + DateAdded = DateTime.UtcNow, + Code = code + }; + + _currentRequests[code] = result; + return result; + } + + /// <inheritdoc/> + public QuickConnectResult CheckRequestStatus(string secret) + { + ExpireRequests(); + AssertActive(); + + string code = _currentRequests.Where(x => x.Value.Secret == secret).Select(x => x.Value.Code).DefaultIfEmpty(string.Empty).First(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request with provided secret"); + } + + return result; + } + + /// <inheritdoc/> + public string GenerateCode() + { + Span<byte> raw = stackalloc byte[4]; + + int min = (int)Math.Pow(10, CodeLength - 1); + int max = (int)Math.Pow(10, CodeLength); + + uint scale = uint.MaxValue; + while (scale == uint.MaxValue) + { + _rng.GetBytes(raw); + scale = BitConverter.ToUInt32(raw); + } + + int code = (int)(min + ((max - min) * (scale / (double)uint.MaxValue))); + return code.ToString(CultureInfo.InvariantCulture); + } + + /// <inheritdoc/> + public bool AuthorizeRequest(Guid userId, string code) + { + ExpireRequests(); + AssertActive(); + + if (!_currentRequests.TryGetValue(code, out QuickConnectResult result)) + { + throw new ResourceNotFoundException("Unable to find request"); + } + + if (result.Authenticated) + { + throw new InvalidOperationException("Request is already authorized"); + } + + result.Authentication = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + + // Change the time on the request so it expires one minute into the future. It can't expire immediately as otherwise some clients wouldn't ever see that they have been authenticated. + var added = result.DateAdded ?? DateTime.UtcNow.Subtract(TimeSpan.FromMinutes(Timeout)); + result.DateAdded = added.Subtract(TimeSpan.FromMinutes(Timeout - 1)); + + _authenticationRepository.Create(new AuthenticationInfo + { + AppName = TokenName, + AccessToken = result.Authentication, + DateCreated = DateTime.UtcNow, + DeviceId = _appHost.SystemId, + DeviceName = _appHost.FriendlyName, + AppVersion = _appHost.ApplicationVersionString, + UserId = userId + }); + + _logger.LogDebug("Authorizing device with code {Code} to login as user {userId}", code, userId); + + return true; + } + + /// <inheritdoc/> + public int DeleteAllDevices(Guid user) + { + var raw = _authenticationRepository.Get(new AuthenticationInfoQuery() + { + DeviceId = _appHost.SystemId, + UserId = user + }); + + var tokens = raw.Items.Where(x => x.AppName.StartsWith(TokenName, StringComparison.Ordinal)); + + var removed = 0; + foreach (var token in tokens) + { + _authenticationRepository.Delete(token); + _logger.LogDebug("Deleted token {AccessToken}", token.AccessToken); + removed++; + } + + return removed; + } + + /// <summary> + /// Dispose. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Dispose. + /// </summary> + /// <param name="disposing">Dispose unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _rng?.Dispose(); + } + } + + private string GenerateSecureRandom(int length = 32) + { + Span<byte> bytes = stackalloc byte[length]; + _rng.GetBytes(bytes); + + return Hex.Encode(bytes); + } + + /// <inheritdoc/> + public void ExpireRequests(bool expireAll = false) + { + // Check if quick connect should be deactivated + if (State == QuickConnectState.Active && DateTime.UtcNow > DateActivated.AddMinutes(Timeout) && !expireAll) + { + _logger.LogDebug("Quick connect time expired, deactivating"); + SetState(QuickConnectState.Available); + expireAll = true; + } + + // Expire stale connection requests + var code = string.Empty; + var values = _currentRequests.Values.ToList(); + + for (int i = 0; i < values.Count; i++) + { + var added = values[i].DateAdded ?? DateTime.UnixEpoch; + if (DateTime.UtcNow > added.AddMinutes(Timeout) || expireAll) + { + code = values[i].Code; + _logger.LogDebug("Removing expired request {code}", code); + + if (!_currentRequests.TryRemove(code, out _)) + { + _logger.LogWarning("Request {code} already expired", code); + } + } + } + } + + private void ReloadConfiguration() + { + State = _config.Configuration.QuickConnectAvailable ? QuickConnectState.Available : QuickConnectState.Unavailable; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index e29fcfb5f..5adcefc1f 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -5,10 +5,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks.Tasks { @@ -21,10 +21,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// Gets or sets the application paths. /// </summary> /// <value>The application paths.</value> - private IApplicationPaths ApplicationPaths { get; set; } - + private readonly IApplicationPaths _applicationPaths; private readonly ILogger<DeleteCacheFileTask> _logger; - private readonly IFileSystem _fileSystem; private readonly ILocalizationManager _localization; @@ -37,20 +35,41 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks IFileSystem fileSystem, ILocalizationManager localization) { - ApplicationPaths = appPaths; + _applicationPaths = appPaths; _logger = logger; _fileSystem = fileSystem; _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskCleanCache"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); + + /// <inheritdoc /> + public string Key => "DeleteCacheFiles"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - + return new[] + { // Every so often new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; @@ -68,7 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -81,7 +100,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress); + DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress); } catch (DirectoryNotFoundException) { @@ -91,7 +110,6 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks return Task.CompletedTask; } - /// <summary> /// Deletes the cache files from directory with a last write time less than a given date. /// </summary> @@ -164,26 +182,5 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks _logger.LogError(ex, "Error deleting file {path}", path); } } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskCleanCache"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskCleanCacheDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory"); - - /// <inheritdoc /> - public string Key => "DeleteCacheFiles"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index 7388086fb..c5af68bce 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -34,6 +34,27 @@ namespace Emby.Server.Implementations.ScheduledTasks _localization = localization; } + /// <inheritdoc /> + public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); + + /// <inheritdoc /> + public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); + + /// <inheritdoc /> + public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); + + /// <inheritdoc /> + public string Key => "PluginUpdates"; + + /// <inheritdoc /> + public bool IsHidden => false; + + /// <inheritdoc /> + public bool IsEnabled => true; + + /// <inheritdoc /> + public bool IsLogged => true; + /// <summary> /// Creates the triggers that define when the task will run. /// </summary> @@ -98,26 +119,5 @@ namespace Emby.Server.Implementations.ScheduledTasks progress.Report(100); } - - /// <inheritdoc /> - public string Name => _localization.GetLocalizedString("TaskUpdatePlugins"); - - /// <inheritdoc /> - public string Description => _localization.GetLocalizedString("TaskUpdatePluginsDescription"); - - /// <inheritdoc /> - public string Category => _localization.GetLocalizedString("TasksApplicationCategory"); - - /// <inheritdoc /> - public string Key => "PluginUpdates"; - - /// <inheritdoc /> - public bool IsHidden => false; - - /// <inheritdoc /> - public bool IsEnabled => true; - - /// <inheritdoc /> - public bool IsLogged => true; } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index eb628ec5f..8b67d37d7 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class DailyTrigger : ITaskTrigger { /// <summary> - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + + /// <summary> + /// Gets or sets the time of day to trigger the task to run. /// </summary> /// <value>The time of day.</value> public TimeSpan TimeOfDay { get; set; } @@ -70,11 +75,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 247a6785a..b04fd7c7e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -11,6 +11,13 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class IntervalTrigger : ITaskTrigger { + private DateTime _lastStartDate; + + /// <summary> + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + /// <summary> /// Gets or sets the interval. /// </summary> @@ -28,8 +35,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <value>The timer.</value> private Timer Timer { get; set; } - private DateTime _lastStartDate; - /// <summary> /// Stars waiting for the trigger action. /// </summary> @@ -89,11 +94,6 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs index 96e5d8897..7cd5493da 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/StartupTrigger.cs @@ -12,6 +12,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// </summary> public class StartupTrigger : ITaskTrigger { + /// <summary> + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + public int DelayMs { get; set; } /// <summary> @@ -49,19 +54,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 4f1bf5c19..0c0ebec08 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -11,7 +11,12 @@ namespace Emby.Server.Implementations.ScheduledTasks public class WeeklyTrigger : ITaskTrigger { /// <summary> - /// Get the time of day to trigger the task to run. + /// Occurs when [triggered]. + /// </summary> + public event EventHandler<EventArgs> Triggered; + + /// <summary> + /// Gets or sets the time of day to trigger the task to run. /// </summary> /// <value>The time of day.</value> public TimeSpan TimeOfDay { get; set; } @@ -96,19 +101,11 @@ namespace Emby.Server.Implementations.ScheduledTasks } /// <summary> - /// Occurs when [triggered]. - /// </summary> - public event EventHandler<EventArgs> Triggered; - - /// <summary> /// Called when [triggered]. /// </summary> private void OnTriggered() { - if (Triggered != null) - { - Triggered(this, EventArgs.Empty); - } + Triggered?.Invoke(this, EventArgs.Empty); } } } diff --git a/Emby.Server.Implementations/Services/HttpResult.cs b/Emby.Server.Implementations/Services/HttpResult.cs deleted file mode 100644 index 8ba86f756..000000000 --- a/Emby.Server.Implementations/Services/HttpResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public class HttpResult - : IHttpResult, IAsyncStreamWriter - { - public HttpResult(object response, string contentType, HttpStatusCode statusCode) - { - this.Headers = new Dictionary<string, string>(); - - this.Response = response; - this.ContentType = contentType; - this.StatusCode = statusCode; - } - - public object Response { get; set; } - - public string ContentType { get; set; } - - public IDictionary<string, string> Headers { get; private set; } - - public int Status { get; set; } - - public HttpStatusCode StatusCode - { - get => (HttpStatusCode)Status; - set => Status = (int)value; - } - - public IRequest RequestContext { get; set; } - - public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) - { - var response = RequestContext?.Response; - - if (this.Response is byte[] bytesResponse) - { - var contentLength = bytesResponse.Length; - - if (response != null) - { - response.ContentLength = contentLength; - } - - if (contentLength > 0) - { - await responseStream.WriteAsync(bytesResponse, 0, contentLength, cancellationToken).ConfigureAwait(false); - } - - return; - } - - await ResponseHelper.WriteObject(this.RequestContext, this.Response, response).ConfigureAwait(false); - } - } -} diff --git a/Emby.Server.Implementations/Services/RequestHelper.cs b/Emby.Server.Implementations/Services/RequestHelper.cs deleted file mode 100644 index 1f9c7fc22..000000000 --- a/Emby.Server.Implementations/Services/RequestHelper.cs +++ /dev/null @@ -1,51 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.IO; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; - -namespace Emby.Server.Implementations.Services -{ - public class RequestHelper - { - public static Func<Type, Stream, Task<object>> GetRequestReader(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.DeserializeXml; - - case "application/json": - case "text/json": - return host.DeserializeJson; - } - - return null; - } - - public static Action<object, Stream> GetResponseWriter(HttpListenerHost host, string contentType) - { - switch (GetContentTypeWithoutEncoding(contentType)) - { - case "application/xml": - case "text/xml": - case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml - return host.SerializeToXml; - - case "application/json": - case "text/json": - return host.SerializeToJson; - } - - return null; - } - - private static string GetContentTypeWithoutEncoding(string contentType) - { - return contentType?.Split(';')[0].ToLowerInvariant().Trim(); - } - } -} diff --git a/Emby.Server.Implementations/Services/ResponseHelper.cs b/Emby.Server.Implementations/Services/ResponseHelper.cs deleted file mode 100644 index a329b531d..000000000 --- a/Emby.Server.Implementations/Services/ResponseHelper.cs +++ /dev/null @@ -1,141 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace Emby.Server.Implementations.Services -{ - public static class ResponseHelper - { - public static Task WriteToResponse(HttpResponse response, IRequest request, object result, CancellationToken cancellationToken) - { - if (result == null) - { - if (response.StatusCode == (int)HttpStatusCode.OK) - { - response.StatusCode = (int)HttpStatusCode.NoContent; - } - - response.ContentLength = 0; - return Task.CompletedTask; - } - - var httpResult = result as IHttpResult; - if (httpResult != null) - { - httpResult.RequestContext = request; - request.ResponseContentType = httpResult.ContentType ?? request.ResponseContentType; - } - - var defaultContentType = request.ResponseContentType; - - if (httpResult != null) - { - if (httpResult.RequestContext == null) - { - httpResult.RequestContext = request; - } - - response.StatusCode = httpResult.Status; - } - - if (result is IHasHeaders responseOptions) - { - foreach (var responseHeaders in responseOptions.Headers) - { - if (string.Equals(responseHeaders.Key, "Content-Length", StringComparison.OrdinalIgnoreCase)) - { - response.ContentLength = long.Parse(responseHeaders.Value, CultureInfo.InvariantCulture); - continue; - } - - response.Headers.Add(responseHeaders.Key, responseHeaders.Value); - } - } - - // ContentType='text/html' is the default for a HttpResponse - // Do not override if another has been set - if (response.ContentType == null || response.ContentType == "text/html") - { - response.ContentType = defaultContentType; - } - - if (response.ContentType == "application/json") - { - response.ContentType += "; charset=utf-8"; - } - - switch (result) - { - case IAsyncStreamWriter asyncStreamWriter: - return asyncStreamWriter.WriteToAsync(response.Body, cancellationToken); - case IStreamWriter streamWriter: - streamWriter.WriteTo(response.Body); - return Task.CompletedTask; - case FileWriter fileWriter: - return fileWriter.WriteToAsync(response, cancellationToken); - case Stream stream: - return CopyStream(stream, response.Body); - case byte[] bytes: - response.ContentType = "application/octet-stream"; - response.ContentLength = bytes.Length; - - if (bytes.Length > 0) - { - return response.Body.WriteAsync(bytes, 0, bytes.Length, cancellationToken); - } - - return Task.CompletedTask; - case string responseText: - var responseTextAsBytes = Encoding.UTF8.GetBytes(responseText); - response.ContentLength = responseTextAsBytes.Length; - - if (responseTextAsBytes.Length > 0) - { - return response.Body.WriteAsync(responseTextAsBytes, 0, responseTextAsBytes.Length, cancellationToken); - } - - return Task.CompletedTask; - } - - return WriteObject(request, result, response); - } - - private static async Task CopyStream(Stream src, Stream dest) - { - using (src) - { - await src.CopyToAsync(dest).ConfigureAwait(false); - } - } - - public static async Task WriteObject(IRequest request, object result, HttpResponse response) - { - var contentType = request.ResponseContentType; - var serializer = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType); - - using (var ms = new MemoryStream()) - { - serializer(result, ms); - - ms.Position = 0; - - var contentLength = ms.Length; - response.ContentLength = contentLength; - - if (contentLength > 0) - { - await ms.CopyToAsync(response.Body).ConfigureAwait(false); - } - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs deleted file mode 100644 index 47e7261e8..000000000 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ /dev/null @@ -1,202 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Model.Services; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public delegate object ActionInvokerFn(object intance, object request); - - public delegate void VoidActionInvokerFn(object intance, object request); - - public class ServiceController - { - private readonly ILogger<ServiceController> _logger; - - /// <summary> - /// Initializes a new instance of the <see cref="ServiceController"/> class. - /// </summary> - /// <param name="logger">The <see cref="ServiceController"/> logger.</param> - public ServiceController(ILogger<ServiceController> logger) - { - _logger = logger; - } - - public void Init(HttpListenerHost appHost, IEnumerable<Type> serviceTypes) - { - foreach (var serviceType in serviceTypes) - { - RegisterService(appHost, serviceType); - } - } - - public void RegisterService(HttpListenerHost appHost, Type serviceType) - { - // Make sure the provided type implements IService - if (!typeof(IService).IsAssignableFrom(serviceType)) - { - _logger.LogWarning("Tried to register a service that does not implement IService: {ServiceType}", serviceType); - return; - } - - var processedReqs = new HashSet<Type>(); - - var actions = ServiceExecGeneral.Reset(serviceType); - - foreach (var mi in serviceType.GetActions()) - { - var requestType = mi.GetParameters()[0].ParameterType; - if (processedReqs.Contains(requestType)) - { - continue; - } - - processedReqs.Add(requestType); - - ServiceExecGeneral.CreateServiceRunnersFor(requestType, actions); - - // var returnMarker = GetTypeWithGenericTypeDefinitionOf(requestType, typeof(IReturn<>)); - // var responseType = returnMarker != null ? - // GetGenericArguments(returnMarker)[0] - // : mi.ReturnType != typeof(object) && mi.ReturnType != typeof(void) ? - // mi.ReturnType - // : Type.GetType(requestType.FullName + "Response"); - - RegisterRestPaths(appHost, requestType, serviceType); - - appHost.AddServiceInfo(serviceType, requestType); - } - } - - public readonly RestPath.RestPathMap RestPathMap = new RestPath.RestPathMap(); - - public void RegisterRestPaths(HttpListenerHost appHost, Type requestType, Type serviceType) - { - var attrs = appHost.GetRouteAttributes(requestType); - foreach (var attr in attrs) - { - var restPath = new RestPath(appHost.CreateInstance, appHost.GetParseFn, requestType, serviceType, attr.Path, attr.Verbs, attr.IsHidden, attr.Summary, attr.Description); - - RegisterRestPath(restPath); - } - } - - private static readonly char[] InvalidRouteChars = new[] { '?', '&' }; - - public void RegisterRestPath(RestPath restPath) - { - if (restPath.Path[0] != '/') - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' must start with a '/'", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Route '{0}' on '{1}' contains invalid chars. ", - restPath.Path, - restPath.RequestType.GetMethodName())); - } - - if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch)) - { - pathsAtFirstMatch.Add(restPath); - } - else - { - RestPathMap[restPath.FirstMatchHashKey] = new List<RestPath>() { restPath }; - } - } - - public RestPath GetRestPathForRequest(string httpMethod, string pathInfo) - { - var matchUsingPathParts = RestPath.GetPathPartsForMatching(pathInfo); - - List<RestPath> firstMatches; - - var yieldedHashMatches = RestPath.GetFirstMatchHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedHashMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - var yieldedWildcardMatches = RestPath.GetFirstMatchWildCardHashKeys(matchUsingPathParts); - foreach (var potentialHashMatch in yieldedWildcardMatches) - { - if (!this.RestPathMap.TryGetValue(potentialHashMatch, out firstMatches)) - { - continue; - } - - var bestScore = -1; - RestPath bestMatch = null; - foreach (var restPath in firstMatches) - { - var score = restPath.MatchScore(httpMethod, matchUsingPathParts); - if (score > bestScore) - { - bestScore = score; - bestMatch = restPath; - } - } - - if (bestScore > 0 && bestMatch != null) - { - return bestMatch; - } - } - - return null; - } - - public Task<object> Execute(HttpListenerHost httpHost, object requestDto, IRequest req) - { - var requestType = requestDto.GetType(); - req.OperationName = requestType.Name; - - var serviceType = httpHost.GetServiceTypeByRequest(requestType); - - var service = httpHost.CreateInstance(serviceType); - - if (service is IRequiresRequest serviceRequiresContext) - { - serviceRequiresContext.Request = req; - } - - // Executes the service and returns the result - return ServiceExecGeneral.Execute(serviceType, req, service, requestDto, requestType.GetMethodName()); - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs deleted file mode 100644 index 7b970627e..000000000 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ /dev/null @@ -1,230 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace Emby.Server.Implementations.Services -{ - public static class ServiceExecExtensions - { - public static string[] AllVerbs = new[] { - "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 - "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 - "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", - "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 - "ORDERPATCH", // RFC 3648 - "ACL", // RFC 3744 - "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ - "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ - "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", - "POLL", "SUBSCRIBE", "UNSUBSCRIBE" - }; - - public static List<MethodInfo> GetActions(this Type serviceType) - { - var list = new List<MethodInfo>(); - - foreach (var mi in serviceType.GetRuntimeMethods()) - { - if (!mi.IsPublic) - { - continue; - } - - if (mi.IsStatic) - { - continue; - } - - if (mi.GetParameters().Length != 1) - { - continue; - } - - var actionName = mi.Name; - if (!AllVerbs.Contains(actionName, StringComparer.OrdinalIgnoreCase)) - { - continue; - } - - list.Add(mi); - } - - return list; - } - } - - internal static class ServiceExecGeneral - { - private static Dictionary<string, ServiceMethod> execMap = new Dictionary<string, ServiceMethod>(); - - public static void CreateServiceRunnersFor(Type requestType, List<ServiceMethod> actions) - { - foreach (var actionCtx in actions) - { - if (execMap.ContainsKey(actionCtx.Id)) - { - continue; - } - - execMap[actionCtx.Id] = actionCtx; - } - } - - public static Task<object> Execute(Type serviceType, IRequest request, object instance, object requestDto, string requestName) - { - var actionName = request.Verb ?? "POST"; - - if (execMap.TryGetValue(ServiceMethod.Key(serviceType, actionName, requestName), out ServiceMethod actionContext)) - { - if (actionContext.RequestFilters != null) - { - foreach (var requestFilter in actionContext.RequestFilters) - { - requestFilter.RequestFilter(request, request.Response, requestDto); - if (request.Response.HasStarted) - { - Task.FromResult<object>(null); - } - } - } - - var response = actionContext.ServiceAction(instance, requestDto); - - if (response is Task taskResponse) - { - return GetTaskResult(taskResponse); - } - - return Task.FromResult(response); - } - - var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException( - string.Format( - CultureInfo.InvariantCulture, - "Could not find method named {1}({0}) or Any({0}) on Service {2}", - requestDto.GetType().GetMethodName(), - expectedMethodName, - serviceType.GetMethodName())); - } - - private static async Task<object> GetTaskResult(Task task) - { - try - { - if (task is Task<object> taskObject) - { - return await taskObject.ConfigureAwait(false); - } - - await task.ConfigureAwait(false); - - var type = task.GetType().GetTypeInfo(); - if (!type.IsGenericType) - { - return null; - } - - var resultProperty = type.GetDeclaredProperty("Result"); - if (resultProperty == null) - { - return null; - } - - var result = resultProperty.GetValue(task); - - // hack alert - if (result.GetType().Name.IndexOf("voidtaskresult", StringComparison.OrdinalIgnoreCase) != -1) - { - return null; - } - - return result; - } - catch (TypeAccessException) - { - return null; // return null for void Task's - } - } - - public static List<ServiceMethod> Reset(Type serviceType) - { - var actions = new List<ServiceMethod>(); - - foreach (var mi in serviceType.GetActions()) - { - var actionName = mi.Name; - var args = mi.GetParameters(); - - var requestType = args[0].ParameterType; - var actionCtx = new ServiceMethod - { - Id = ServiceMethod.Key(serviceType, actionName, requestType.GetMethodName()) - }; - - actionCtx.ServiceAction = CreateExecFn(serviceType, requestType, mi); - - var reqFilters = new List<IHasRequestFilter>(); - - foreach (var attr in mi.GetCustomAttributes(true)) - { - if (attr is IHasRequestFilter hasReqFilter) - { - reqFilters.Add(hasReqFilter); - } - } - - if (reqFilters.Count > 0) - { - actionCtx.RequestFilters = reqFilters.OrderBy(i => i.Priority).ToArray(); - } - - actions.Add(actionCtx); - } - - return actions; - } - - private static ActionInvokerFn CreateExecFn(Type serviceType, Type requestType, MethodInfo mi) - { - var serviceParam = Expression.Parameter(typeof(object), "serviceObj"); - var serviceStrong = Expression.Convert(serviceParam, serviceType); - - var requestDtoParam = Expression.Parameter(typeof(object), "requestDto"); - var requestDtoStrong = Expression.Convert(requestDtoParam, requestType); - - Expression callExecute = Expression.Call( - serviceStrong, mi, requestDtoStrong); - - if (mi.ReturnType != typeof(void)) - { - var executeFunc = Expression.Lambda<ActionInvokerFn>( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return executeFunc; - } - else - { - var executeFunc = Expression.Lambda<VoidActionInvokerFn>( - callExecute, - serviceParam, - requestDtoParam).Compile(); - - return (service, request) => - { - executeFunc(service, request); - return null; - }; - } - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs deleted file mode 100644 index b4166f771..000000000 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Net.Mime; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Emby.Server.Implementations.HttpServer; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceHandler - { - private RestPath _restPath; - - private string _responseContentType; - - internal ServiceHandler(RestPath restPath, string responseContentType) - { - _restPath = restPath; - _responseContentType = responseContentType; - } - - protected static Task<object> CreateContentTypeRequest(HttpListenerHost host, IRequest httpReq, Type requestType, string contentType) - { - if (!string.IsNullOrEmpty(contentType) && httpReq.ContentLength > 0) - { - var deserializer = RequestHelper.GetRequestReader(host, contentType); - if (deserializer != null) - { - return deserializer.Invoke(requestType, httpReq.InputStream); - } - } - - return Task.FromResult(host.CreateInstance(requestType)); - } - - public static string GetSanitizedPathInfo(string pathInfo, out string contentType) - { - contentType = null; - var pos = pathInfo.LastIndexOf('.'); - if (pos != -1) - { - var format = pathInfo.AsSpan().Slice(pos + 1); - contentType = GetFormatContentType(format); - if (contentType != null) - { - pathInfo = pathInfo.Substring(0, pos); - } - } - - return pathInfo; - } - - private static string GetFormatContentType(ReadOnlySpan<char> format) - { - if (format.Equals("json", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Json; - } - else if (format.Equals("xml", StringComparison.Ordinal)) - { - return MediaTypeNames.Application.Xml; - } - - return null; - } - - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken) - { - httpReq.Items["__route"] = _restPath; - - if (_responseContentType != null) - { - httpReq.ResponseContentType = _responseContentType; - } - - var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false); - - httpHost.ApplyRequestFilters(httpReq, httpRes, request); - - httpRes.HttpContext.SetServiceStackRequest(httpReq); - var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); - - // Apply response filters - foreach (var responseFilter in httpHost.ResponseFilters) - { - responseFilter(httpReq, httpRes, response); - } - - await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); - } - - public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath) - { - var requestType = restPath.RequestType; - - if (RequireqRequestStream(requestType)) - { - // Used by IRequiresRequestStream - var requestParams = GetRequestParams(httpReq.Response.HttpContext.Request); - var request = ServiceHandler.CreateRequest(httpReq, restPath, requestParams, host.CreateInstance(requestType)); - - var rawReq = (IRequiresRequestStream)request; - rawReq.RequestStream = httpReq.InputStream; - return rawReq; - } - else - { - var requestParams = GetFlattenedRequestParams(httpReq.Response.HttpContext.Request); - - var requestDto = await CreateContentTypeRequest(host, httpReq, restPath.RequestType, httpReq.ContentType).ConfigureAwait(false); - - return CreateRequest(httpReq, restPath, requestParams, requestDto); - } - } - - public static bool RequireqRequestStream(Type requestType) - { - var requiresRequestStreamTypeInfo = typeof(IRequiresRequestStream).GetTypeInfo(); - - return requiresRequestStreamTypeInfo.IsAssignableFrom(requestType.GetTypeInfo()); - } - - public static object CreateRequest(IRequest httpReq, RestPath restPath, Dictionary<string, string> requestParams, object requestDto) - { - var pathInfo = !restPath.IsWildCardPath - ? GetSanitizedPathInfo(httpReq.PathInfo, out _) - : httpReq.PathInfo; - - return restPath.CreateRequest(pathInfo, requestParams, requestDto); - } - - /// <summary> - /// Duplicate Params are given a unique key by appending a #1 suffix - /// </summary> - private static Dictionary<string, string> GetRequestParams(HttpRequest request) - { - var map = new Dictionary<string, string>(); - - foreach (var pair in request.Query) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - var values = pair.Value; - if (values.Count == 1) - { - map[pair.Key] = values[0]; - } - else - { - for (var i = 0; i < values.Count; i++) - { - map[pair.Key + (i == 0 ? string.Empty : "#" + i)] = values[i]; - } - } - } - } - - return map; - } - - private static bool IsMethod(string method, string expected) - => string.Equals(method, expected, StringComparison.OrdinalIgnoreCase); - - /// <summary> - /// Duplicate params have their values joined together in a comma-delimited string. - /// </summary> - private static Dictionary<string, string> GetFlattenedRequestParams(HttpRequest request) - { - var map = new Dictionary<string, string>(); - - foreach (var pair in request.Query) - { - map[pair.Key] = pair.Value; - } - - if ((IsMethod(request.Method, "POST") || IsMethod(request.Method, "PUT")) - && request.HasFormContentType) - { - foreach (var pair in request.Form) - { - map[pair.Key] = pair.Value; - } - } - - return map; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServiceMethod.cs b/Emby.Server.Implementations/Services/ServiceMethod.cs deleted file mode 100644 index 5116cc04f..000000000 --- a/Emby.Server.Implementations/Services/ServiceMethod.cs +++ /dev/null @@ -1,20 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Emby.Server.Implementations.Services -{ - public class ServiceMethod - { - public string Id { get; set; } - - public ActionInvokerFn ServiceAction { get; set; } - - public MediaBrowser.Model.Services.IHasRequestFilter[] RequestFilters { get; set; } - - public static string Key(Type serviceType, string method, string requestDtoName) - { - return serviceType.FullName + " " + method.ToUpperInvariant() + " " + requestDtoName; - } - } -} diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs deleted file mode 100644 index 0d4728b43..000000000 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ /dev/null @@ -1,550 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Text.Json.Serialization; - -namespace Emby.Server.Implementations.Services -{ - public class RestPath - { - private const string WildCard = "*"; - private const char WildCardChar = '*'; - private const string PathSeperator = "/"; - private const char PathSeperatorChar = '/'; - private const char ComponentSeperator = '.'; - private const string VariablePrefix = "{"; - - private readonly bool[] componentsWithSeparators; - - private readonly string restPath; - public bool IsWildCardPath { get; private set; } - - private readonly string[] literalsToMatch; - - private readonly string[] variablesNames; - - private readonly bool[] isWildcard; - private readonly int wildcardCount = 0; - - internal static string[] IgnoreAttributesNamed = new[] - { - nameof(JsonIgnoreAttribute) - }; - - private static Type _excludeType = typeof(Stream); - - public int VariableArgsCount { get; set; } - - /// <summary> - /// The number of segments separated by '/' determinable by path.Split('/').Length - /// e.g. /path/to/here.ext == 3 - /// </summary> - public int PathComponentsCount { get; set; } - - /// <summary> - /// Gets or sets the total number of segments after subparts have been exploded ('.') - /// e.g. /path/to/here.ext == 4. - /// </summary> - public int TotalComponentsCount { get; set; } - - public string[] Verbs { get; private set; } - - public Type RequestType { get; private set; } - - public Type ServiceType { get; private set; } - - public string Path => this.restPath; - - public string Summary { get; private set; } - - public string Description { get; private set; } - - public bool IsHidden { get; private set; } - - public static string[] GetPathPartsForMatching(string pathInfo) - { - return pathInfo.ToLowerInvariant().Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - } - - public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching) - { - var hashPrefix = pathPartsForMatching.Length + PathSeperator; - return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching); - } - - public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching) - { - const string HashPrefix = WildCard + PathSeperator; - return GetPotentialMatchesWithPrefix(HashPrefix, pathPartsForMatching); - } - - private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching) - { - var list = new List<string>(); - - foreach (var part in pathPartsForMatching) - { - list.Add(hashPrefix + part); - - if (part.IndexOf(ComponentSeperator, StringComparison.Ordinal) == -1) - { - continue; - } - - var subParts = part.Split(ComponentSeperator); - foreach (var subPart in subParts) - { - list.Add(hashPrefix + subPart); - } - } - - return list; - } - - public RestPath(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type requestType, Type serviceType, string path, string verbs, bool isHidden = false, string summary = null, string description = null) - { - this.RequestType = requestType; - this.ServiceType = serviceType; - this.Summary = summary; - this.IsHidden = isHidden; - this.Description = description; - this.restPath = path; - - this.Verbs = string.IsNullOrWhiteSpace(verbs) ? ServiceExecExtensions.AllVerbs : verbs.ToUpperInvariant().Split(new[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries); - - var componentsList = new List<string>(); - - // We only split on '.' if the restPath has them. Allows for /{action}.{type} - var hasSeparators = new List<bool>(); - foreach (var component in this.restPath.Split(PathSeperatorChar)) - { - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (component.IndexOf(VariablePrefix, StringComparison.OrdinalIgnoreCase) != -1 - && component.IndexOf(ComponentSeperator, StringComparison.Ordinal) != -1) - { - hasSeparators.Add(true); - componentsList.AddRange(component.Split(ComponentSeperator)); - } - else - { - hasSeparators.Add(false); - componentsList.Add(component); - } - } - - var components = componentsList.ToArray(); - this.TotalComponentsCount = components.Length; - - this.literalsToMatch = new string[this.TotalComponentsCount]; - this.variablesNames = new string[this.TotalComponentsCount]; - this.isWildcard = new bool[this.TotalComponentsCount]; - this.componentsWithSeparators = hasSeparators.ToArray(); - this.PathComponentsCount = this.componentsWithSeparators.Length; - string firstLiteralMatch = null; - - for (var i = 0; i < components.Length; i++) - { - var component = components[i]; - - if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) - { - var variableName = component.Substring(1, component.Length - 2); - if (variableName[variableName.Length - 1] == WildCardChar) - { - this.isWildcard[i] = true; - variableName = variableName.Substring(0, variableName.Length - 1); - } - - this.variablesNames[i] = variableName; - this.VariableArgsCount++; - } - else - { - this.literalsToMatch[i] = component.ToLowerInvariant(); - - if (firstLiteralMatch == null) - { - firstLiteralMatch = this.literalsToMatch[i]; - } - } - } - - for (var i = 0; i < components.Length - 1; i++) - { - if (!this.isWildcard[i]) - { - continue; - } - - if (this.literalsToMatch[i + 1] == null) - { - throw new ArgumentException( - "A wildcard path component must be at the end of the path or followed by a literal path component."); - } - } - - this.wildcardCount = this.isWildcard.Length; - this.IsWildCardPath = this.wildcardCount > 0; - - this.FirstMatchHashKey = !this.IsWildCardPath - ? this.PathComponentsCount + PathSeperator + firstLiteralMatch - : WildCardChar + PathSeperator + firstLiteralMatch; - - this.typeDeserializer = new StringMapTypeDeserializer(createInstanceFn, getParseFn, this.RequestType); - - _propertyNamesMap = new HashSet<string>( - GetSerializableProperties(RequestType).Select(x => x.Name), - StringComparer.OrdinalIgnoreCase); - } - - internal static IEnumerable<PropertyInfo> GetSerializableProperties(Type type) - { - foreach (var prop in GetPublicProperties(type)) - { - if (prop.GetMethod == null - || _excludeType == prop.PropertyType) - { - continue; - } - - var ignored = false; - foreach (var attr in prop.GetCustomAttributes(true)) - { - if (IgnoreAttributesNamed.Contains(attr.GetType().Name)) - { - ignored = true; - break; - } - } - - if (!ignored) - { - yield return prop; - } - } - } - - private static IEnumerable<PropertyInfo> GetPublicProperties(Type type) - { - if (type.IsInterface) - { - var propertyInfos = new List<PropertyInfo>(); - var considered = new List<Type>() - { - type - }; - var queue = new Queue<Type>(); - queue.Enqueue(type); - - while (queue.Count > 0) - { - var subType = queue.Dequeue(); - foreach (var subInterface in subType.GetTypeInfo().ImplementedInterfaces) - { - if (considered.Contains(subInterface)) - { - continue; - } - - considered.Add(subInterface); - queue.Enqueue(subInterface); - } - - var newPropertyInfos = GetTypesPublicProperties(subType) - .Where(x => !propertyInfos.Contains(x)); - - propertyInfos.InsertRange(0, newPropertyInfos); - } - - return propertyInfos; - } - - return GetTypesPublicProperties(type) - .Where(x => x.GetIndexParameters().Length == 0); - } - - private static IEnumerable<PropertyInfo> GetTypesPublicProperties(Type subType) - { - foreach (var pi in subType.GetRuntimeProperties()) - { - var mi = pi.GetMethod ?? pi.SetMethod; - if (mi != null && mi.IsStatic) - { - continue; - } - - yield return pi; - } - } - - /// <summary> - /// Provide for quick lookups based on hashes that can be determined from a request url. - /// </summary> - public string FirstMatchHashKey { get; private set; } - - private readonly StringMapTypeDeserializer typeDeserializer; - - private readonly HashSet<string> _propertyNamesMap; - - public int MatchScore(string httpMethod, string[] withPathInfoParts) - { - var isMatch = IsMatch(httpMethod, withPathInfoParts, out var wildcardMatchCount); - if (!isMatch) - { - return -1; - } - - // Routes with least wildcard matches get the highest score - var score = Math.Max(100 - wildcardMatchCount, 1) * 1000 - // Routes with less variable (and more literal) matches - + Math.Max(10 - VariableArgsCount, 1) * 100; - - // Exact verb match is better than ANY - if (Verbs.Length == 1 && string.Equals(httpMethod, Verbs[0], StringComparison.OrdinalIgnoreCase)) - { - score += 10; - } - else - { - score += 1; - } - - return score; - } - - /// <summary> - /// For performance withPathInfoParts should already be a lower case string - /// to minimize redundant matching operations. - /// </summary> - public bool IsMatch(string httpMethod, string[] withPathInfoParts, out int wildcardMatchCount) - { - wildcardMatchCount = 0; - - if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath) - { - return false; - } - - if (!Verbs.Contains(httpMethod, StringComparer.OrdinalIgnoreCase)) - { - return false; - } - - if (!ExplodeComponents(ref withPathInfoParts)) - { - return false; - } - - if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath) - { - return false; - } - - int pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - if (this.isWildcard[i]) - { - if (i < this.TotalComponentsCount - 1) - { - // Continue to consume up until a match with the next literal - while (pathIx < withPathInfoParts.Length - && !string.Equals(withPathInfoParts[pathIx], this.literalsToMatch[i + 1], StringComparison.InvariantCultureIgnoreCase)) - { - pathIx++; - wildcardMatchCount++; - } - - // Ensure there are still enough parts left to match the remainder - if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1)) - { - return false; - } - } - else - { - // A wildcard at the end matches the remainder of path - wildcardMatchCount += withPathInfoParts.Length - pathIx; - pathIx = withPathInfoParts.Length; - } - } - else - { - var literalToMatch = this.literalsToMatch[i]; - if (literalToMatch == null) - { - // Matching an ordinary (non-wildcard) variable consumes a single part - pathIx++; - continue; - } - - if (withPathInfoParts.Length <= pathIx - || !string.Equals(withPathInfoParts[pathIx], literalToMatch, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - pathIx++; - } - } - - return pathIx == withPathInfoParts.Length; - } - - private bool ExplodeComponents(ref string[] withPathInfoParts) - { - var totalComponents = new List<string>(); - for (var i = 0; i < withPathInfoParts.Length; i++) - { - var component = withPathInfoParts[i]; - if (string.IsNullOrEmpty(component)) - { - continue; - } - - if (this.PathComponentsCount != this.TotalComponentsCount - && this.componentsWithSeparators[i]) - { - var subComponents = component.Split(ComponentSeperator); - if (subComponents.Length < 2) - { - return false; - } - - totalComponents.AddRange(subComponents); - } - else - { - totalComponents.Add(component); - } - } - - withPathInfoParts = totalComponents.ToArray(); - return true; - } - - public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance) - { - var requestComponents = pathInfo.Split(new[] { PathSeperatorChar }, StringSplitOptions.RemoveEmptyEntries); - - ExplodeComponents(ref requestComponents); - - if (requestComponents.Length != this.TotalComponentsCount) - { - var isValidWildCardPath = this.IsWildCardPath - && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount; - - if (!isValidWildCardPath) - { - throw new ArgumentException( - string.Format( - CultureInfo.InvariantCulture, - "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'", - pathInfo, - this.restPath)); - } - } - - var requestKeyValuesMap = new Dictionary<string, string>(); - var pathIx = 0; - for (var i = 0; i < this.TotalComponentsCount; i++) - { - var variableName = this.variablesNames[i]; - if (variableName == null) - { - pathIx++; - continue; - } - - if (!this._propertyNamesMap.Contains(variableName)) - { - if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase)) - { - pathIx++; - continue; - } - - throw new ArgumentException("Could not find property " - + variableName + " on " + RequestType.GetMethodName()); - } - - var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; // wildcard has arg mismatch - if (value != null && this.isWildcard[i]) - { - if (i == this.TotalComponentsCount - 1) - { - // Wildcard at end of path definition consumes all the rest - var sb = new StringBuilder(); - sb.Append(value); - for (var j = pathIx + 1; j < requestComponents.Length; j++) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[j]); - } - - value = sb.ToString(); - } - else - { - // Wildcard in middle of path definition consumes up until it - // hits a match for the next element in the definition (which must be a literal) - // It may consume 0 or more path parts - var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1]; - if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - var sb = new StringBuilder(value); - pathIx++; - while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase)) - { - sb.Append(PathSeperatorChar) - .Append(requestComponents[pathIx++]); - } - - value = sb.ToString(); - } - else - { - value = null; - } - } - } - else - { - // Variable consumes single path item - pathIx++; - } - - requestKeyValuesMap[variableName] = value; - } - - if (queryStringAndFormData != null) - { - // Query String and form data can override variable path matches - // path variables < query string < form data - foreach (var name in queryStringAndFormData) - { - requestKeyValuesMap[name.Key] = name.Value; - } - } - - return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap); - } - - public class RestPathMap : SortedDictionary<string, List<RestPath>> - { - public RestPathMap() : base(StringComparer.OrdinalIgnoreCase) - { - } - } - } -} diff --git a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs b/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs deleted file mode 100644 index 165bb0fc4..000000000 --- a/Emby.Server.Implementations/Services/StringMapTypeDeserializer.cs +++ /dev/null @@ -1,118 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Reflection; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// <summary> - /// Serializer cache of delegates required to create a type from a string map (e.g. for REST urls) - /// </summary> - public class StringMapTypeDeserializer - { - internal class PropertySerializerEntry - { - public PropertySerializerEntry(Action<object, object> propertySetFn, Func<string, object> propertyParseStringFn, Type propertyType) - { - PropertySetFn = propertySetFn; - PropertyParseStringFn = propertyParseStringFn; - PropertyType = propertyType; - } - - public Action<object, object> PropertySetFn { get; private set; } - - public Func<string, object> PropertyParseStringFn { get; private set; } - - public Type PropertyType { get; private set; } - } - - private readonly Type type; - private readonly Dictionary<string, PropertySerializerEntry> propertySetterMap - = new Dictionary<string, PropertySerializerEntry>(StringComparer.OrdinalIgnoreCase); - - public Func<string, object> GetParseFn(Type propertyType) - { - if (propertyType == typeof(string)) - { - return s => s; - } - - return _GetParseFn(propertyType); - } - - private readonly Func<Type, object> _CreateInstanceFn; - private readonly Func<Type, Func<string, object>> _GetParseFn; - - public StringMapTypeDeserializer(Func<Type, object> createInstanceFn, Func<Type, Func<string, object>> getParseFn, Type type) - { - _CreateInstanceFn = createInstanceFn; - _GetParseFn = getParseFn; - this.type = type; - - foreach (var propertyInfo in RestPath.GetSerializableProperties(type)) - { - var propertySetFn = TypeAccessor.GetSetPropertyMethod(propertyInfo); - var propertyType = propertyInfo.PropertyType; - var propertyParseStringFn = GetParseFn(propertyType); - var propertySerializer = new PropertySerializerEntry(propertySetFn, propertyParseStringFn, propertyType); - - propertySetterMap[propertyInfo.Name] = propertySerializer; - } - } - - public object PopulateFromMap(object instance, IDictionary<string, string> keyValuePairs) - { - PropertySerializerEntry propertySerializerEntry = null; - - if (instance == null) - { - instance = _CreateInstanceFn(type); - } - - foreach (var pair in keyValuePairs) - { - string propertyName = pair.Key; - string propertyTextValue = pair.Value; - - if (propertyTextValue == null - || !propertySetterMap.TryGetValue(propertyName, out propertySerializerEntry) - || propertySerializerEntry.PropertySetFn == null) - { - continue; - } - - if (propertySerializerEntry.PropertyType == typeof(bool)) - { - // InputExtensions.cs#530 MVC Checkbox helper emits extra hidden input field, generating 2 values, first is the real value - propertyTextValue = StringExtensions.LeftPart(propertyTextValue, ',').ToString(); - } - - var value = propertySerializerEntry.PropertyParseStringFn(propertyTextValue); - if (value == null) - { - continue; - } - - propertySerializerEntry.PropertySetFn(instance, value); - } - - return instance; - } - } - - internal static class TypeAccessor - { - public static Action<object, object> GetSetPropertyMethod(PropertyInfo propertyInfo) - { - if (!propertyInfo.CanWrite || propertyInfo.GetIndexParameters().Length > 0) - { - return null; - } - - var setMethodInfo = propertyInfo.SetMethod; - return (instance, value) => setMethodInfo.Invoke(instance, new[] { value }); - } - } -} diff --git a/Emby.Server.Implementations/Services/UrlExtensions.cs b/Emby.Server.Implementations/Services/UrlExtensions.cs deleted file mode 100644 index 92e36b60e..000000000 --- a/Emby.Server.Implementations/Services/UrlExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Common.Extensions; - -namespace Emby.Server.Implementations.Services -{ - /// <summary> - /// Donated by Ivan Korneliuk from his post: - /// http://korneliuk.blogspot.com/2012/08/servicestack-reusing-dtos.html - /// - /// Modified to only allow using routes matching the supplied HTTP Verb. - /// </summary> - public static class UrlExtensions - { - public static string GetMethodName(this Type type) - { - var typeName = type.FullName != null // can be null, e.g. generic types - ? StringExtensions.LeftPart(type.FullName, "[[", StringComparison.Ordinal).ToString() // Generic Fullname - .Replace(type.Namespace + ".", string.Empty, StringComparison.Ordinal) // Trim Namespaces - .Replace("+", ".", StringComparison.Ordinal) // Convert nested into normal type - : type.Name; - - return type.IsGenericParameter ? "'" + typeName : typeName; - } - } -} diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index dad414142..ca8e0e29b 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -1429,6 +1429,24 @@ namespace Emby.Server.Implementations.Session return AuthenticateNewSessionInternal(request, false); } + public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token) + { + var result = _authRepo.Get(new AuthenticationInfoQuery() + { + AccessToken = token, + DeviceId = _appHost.SystemId, + Limit = 1 + }); + + if (result.TotalRecordCount == 0) + { + throw new SecurityException("Unknown quick connect token"); + } + + request.UserId = result.Items[0].UserId; + return AuthenticateNewSessionInternal(request, false); + } + private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index 1da7a6473..15c2af220 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -44,7 +44,7 @@ namespace Emby.Server.Implementations.Session private readonly ILogger<SessionWebSocketListener> _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IHttpServer _httpServer; + private readonly IWebSocketManager _webSocketManager; /// <summary> /// The KeepAlive cancellation token. @@ -72,19 +72,19 @@ namespace Emby.Server.Implementations.Session /// <param name="logger">The logger.</param> /// <param name="sessionManager">The session manager.</param> /// <param name="loggerFactory">The logger factory.</param> - /// <param name="httpServer">The HTTP server.</param> + /// <param name="webSocketManager">The HTTP server.</param> public SessionWebSocketListener( ILogger<SessionWebSocketListener> logger, ISessionManager sessionManager, ILoggerFactory loggerFactory, - IHttpServer httpServer) + IWebSocketManager webSocketManager) { _logger = logger; _sessionManager = sessionManager; _loggerFactory = loggerFactory; - _httpServer = httpServer; + _webSocketManager = webSocketManager; - httpServer.WebSocketConnected += OnServerManagerWebSocketConnected; + webSocketManager.WebSocketConnected += OnServerManagerWebSocketConnected; } private async void OnServerManagerWebSocketConnected(object sender, GenericEventArgs<IWebSocketConnection> e) @@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.Session /// <inheritdoc /> public void Dispose() { - _httpServer.WebSocketConnected -= OnServerManagerWebSocketConnected; + _webSocketManager.WebSocketConnected -= OnServerManagerWebSocketConnected; StopKeepAlive(); } diff --git a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs deleted file mode 100644 index ae1a8d0b7..000000000 --- a/Emby.Server.Implementations/SocketSharp/WebSocketSharpRequest.cs +++ /dev/null @@ -1,248 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Mime; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.Net; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; - -namespace Emby.Server.Implementations.SocketSharp -{ - public class WebSocketSharpRequest : IHttpRequest - { - private const string FormUrlEncoded = "application/x-www-form-urlencoded"; - private const string MultiPartFormData = "multipart/form-data"; - private const string Soap11 = "text/xml; charset=utf-8"; - - private string _remoteIp; - private Dictionary<string, object> _items; - private string _responseContentType; - - public WebSocketSharpRequest(HttpRequest httpRequest, HttpResponse httpResponse, string operationName) - { - this.OperationName = operationName; - this.Request = httpRequest; - this.Response = httpResponse; - } - - public string Accept => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Accept]) ? null : Request.Headers[HeaderNames.Accept].ToString(); - - public string Authorization => StringValues.IsNullOrEmpty(Request.Headers[HeaderNames.Authorization]) ? null : Request.Headers[HeaderNames.Authorization].ToString(); - - public HttpRequest Request { get; } - - public HttpResponse Response { get; } - - public string OperationName { get; set; } - - public string RawUrl => Request.GetEncodedPathAndQuery(); - - public string AbsoluteUri => Request.GetDisplayUrl().TrimEnd('/'); - - public string RemoteIp - { - get - { - if (_remoteIp != null) - { - return _remoteIp; - } - - IPAddress ip; - - // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip - // (if the server is behind a reverse proxy for example) - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XForwardedFor), out ip)) - { - if (!IPAddress.TryParse(GetHeader(CustomHeaderNames.XRealIP), out ip)) - { - ip = Request.HttpContext.Connection.RemoteIpAddress; - - // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) - ip ??= IPAddress.Loopback; - } - } - - return _remoteIp = NormalizeIp(ip).ToString(); - } - } - - public string[] AcceptTypes => Request.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - - public Dictionary<string, object> Items => _items ?? (_items = new Dictionary<string, object>()); - - public string ResponseContentType - { - get => - _responseContentType - ?? (_responseContentType = GetResponseContentType(Request)); - set => _responseContentType = value; - } - - public string PathInfo => Request.Path.Value; - - public string UserAgent => Request.Headers[HeaderNames.UserAgent]; - - public IHeaderDictionary Headers => Request.Headers; - - public IQueryCollection QueryString => Request.Query; - - public bool IsLocal => - (Request.HttpContext.Connection.LocalIpAddress == null - && Request.HttpContext.Connection.RemoteIpAddress == null) - || Request.HttpContext.Connection.LocalIpAddress.Equals(Request.HttpContext.Connection.RemoteIpAddress); - - public string HttpMethod => Request.Method; - - public string Verb => HttpMethod; - - public string ContentType => Request.ContentType; - - public Uri UrlReferrer => Request.GetTypedHeaders().Referer; - - public Stream InputStream => Request.Body; - - public long ContentLength => Request.ContentLength ?? 0; - - private string GetHeader(string name) => Request.Headers[name].ToString(); - - private static IPAddress NormalizeIp(IPAddress ip) - { - if (ip.IsIPv4MappedToIPv6) - { - return ip.MapToIPv4(); - } - - return ip; - } - - public static string GetResponseContentType(HttpRequest httpReq) - { - var specifiedContentType = GetQueryStringContentType(httpReq); - if (!string.IsNullOrEmpty(specifiedContentType)) - { - return specifiedContentType; - } - - const string ServerDefaultContentType = MediaTypeNames.Application.Json; - - var acceptContentTypes = httpReq.Headers.GetCommaSeparatedValues(HeaderNames.Accept); - string defaultContentType = null; - if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) - { - defaultContentType = ServerDefaultContentType; - } - - var acceptsAnything = false; - var hasDefaultContentType = defaultContentType != null; - if (acceptContentTypes != null) - { - foreach (ReadOnlySpan<char> acceptsType in acceptContentTypes) - { - ReadOnlySpan<char> contentType = acceptsType; - var index = contentType.IndexOf(';'); - if (index != -1) - { - contentType = contentType.Slice(0, index); - } - - contentType = contentType.Trim(); - acceptsAnything = contentType.Equals("*/*", StringComparison.OrdinalIgnoreCase); - - if (acceptsAnything) - { - break; - } - } - - if (acceptsAnything) - { - if (hasDefaultContentType) - { - return defaultContentType; - } - else - { - return ServerDefaultContentType; - } - } - } - - if (acceptContentTypes == null && httpReq.ContentType == Soap11) - { - return Soap11; - } - - // We could also send a '406 Not Acceptable', but this is allowed also - return ServerDefaultContentType; - } - - public static bool HasAnyOfContentTypes(HttpRequest request, params string[] contentTypes) - { - if (contentTypes == null || request.ContentType == null) - { - return false; - } - - foreach (var contentType in contentTypes) - { - if (IsContentType(request, contentType)) - { - return true; - } - } - - return false; - } - - public static bool IsContentType(HttpRequest request, string contentType) - { - return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); - } - - private static string GetQueryStringContentType(HttpRequest httpReq) - { - ReadOnlySpan<char> format = httpReq.Query["format"].ToString(); - if (format == ReadOnlySpan<char>.Empty) - { - const int FormatMaxLength = 4; - ReadOnlySpan<char> pi = httpReq.Path.ToString(); - if (pi == null || pi.Length <= FormatMaxLength) - { - return null; - } - - if (pi[0] == '/') - { - pi = pi.Slice(1); - } - - format = pi.LeftPart('/'); - if (format.Length > FormatMaxLength) - { - return null; - } - } - - format = format.LeftPart('.'); - if (format.Contains("json", StringComparison.OrdinalIgnoreCase)) - { - return "application/json"; - } - else if (format.Contains("xml", StringComparison.OrdinalIgnoreCase)) - { - return "application/xml"; - } - - return null; - } - } -} diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs index ee87d917b..6a0c3d5d3 100644 --- a/Jellyfin.Api/Controllers/DlnaServerController.cs +++ b/Jellyfin.Api/Controllers/DlnaServerController.cs @@ -61,7 +61,8 @@ namespace Jellyfin.Api.Controllers /// <response code="200">Dlna content directory returned.</response> /// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns> [HttpGet("{serverId}/ContentDirectory")] - [HttpGet("{serverId}/ContentDirectory.xml", Name = "GetContentDirectory_2")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")] + [HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -76,7 +77,8 @@ namespace Jellyfin.Api.Controllers /// <param name="serverId">Server UUID.</param> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/MediaReceiverRegistrar")] - [HttpGet("{serverId}/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_2")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")] + [HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] @@ -91,7 +93,8 @@ namespace Jellyfin.Api.Controllers /// <param name="serverId">Server UUID.</param> /// <returns>Dlna media receiver registrar xml.</returns> [HttpGet("{serverId}/ConnectionManager")] - [HttpGet("{serverId}/ConnectionManager.xml", Name = "GetConnectionManager_2")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")] + [HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")] [Produces(XMLContentType)] [ProducesResponseType(StatusCodes.Status200OK)] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")] diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index b115ac6cd..0c884d58d 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1354,15 +1354,20 @@ namespace Jellyfin.Api.Controllers segmentFormat = "mpegts"; } + var maxMuxingQueueSize = encodingOptions.MaxMuxingQueueSize > 128 + ? encodingOptions.MaxMuxingQueueSize.ToString(CultureInfo.InvariantCulture) + : "128"; + return string.Format( CultureInfo.InvariantCulture, - "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size 2048 -f hls -max_delay 5000000 -hls_time {6} -individual_header_trailer 0 -hls_segment_type {7} -start_number {8} -hls_segment_filename \"{9}\" -hls_playlist_type vod -hls_list_size 0 -y \"{10}\"", + "{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -max_muxing_queue_size {6} -f hls -max_delay 5000000 -hls_time {7} -individual_header_trailer 0 -hls_segment_type {8} -start_number {9} -hls_segment_filename \"{10}\" -hls_playlist_type vod -hls_list_size 0 -y \"{11}\"", inputModifier, _encodingHelper.GetInputArgument(state, encodingOptions), threads, mapArgs, GetVideoArguments(state, encodingOptions, startNumber), GetAudioArguments(state, encodingOptions), + maxMuxingQueueSize, state.SegmentLength.ToString(CultureInfo.InvariantCulture), segmentFormat, startNumberParam, diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs index ca9c2fa46..a204fe35c 100644 --- a/Jellyfin.Api/Controllers/ImageController.cs +++ b/Jellyfin.Api/Controllers/ImageController.cs @@ -1281,9 +1281,9 @@ namespace Jellyfin.Api.Controllers Response.Headers.Add(HeaderNames.LastModified, dateImageModified.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss \"GMT\"", new CultureInfo("en-US", false))); // if the image was not modified since "ifModifiedSinceHeader"-header, return a HTTP status code 304 not modified - if (!(dateImageModified > ifModifiedSinceHeader)) + if (!(dateImageModified > ifModifiedSinceHeader) && cacheDuration.HasValue) { - if (ifModifiedSinceHeader.Add(cacheDuration!.Value) < DateTime.UtcNow) + if (ifModifiedSinceHeader.Add(cacheDuration.Value) < DateTime.UtcNow) { Response.StatusCode = StatusCodes.Status304NotModified; return new ContentResult(); diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 1b8b68313..f9273bad6 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -4,10 +4,10 @@ using System.Linq; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; -using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -266,7 +266,9 @@ namespace Jellyfin.Api.Controllers bool isInEnabledFolder = user!.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id) // Assume all folders inside an EnabledChannel are enabled - || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id); + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id) + // Assume all items inside an EnabledChannel are enabled + || user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.ChannelId); var collectionFolders = _libraryManager.GetCollectionFolders(item); foreach (var collectionFolder in collectionFolders) diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index 796d2d8aa..a30873e9e 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Net; @@ -35,8 +36,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; -using Movie = Jellyfin.Data.Entities.Movie; -using MusicAlbum = Jellyfin.Data.Entities.MusicAlbum; namespace Jellyfin.Api.Controllers { diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs index cdab4f356..d290e3c5b 100644 --- a/Jellyfin.Api/Controllers/LibraryStructureController.cs +++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs @@ -76,7 +76,7 @@ namespace Jellyfin.Api.Controllers [FromQuery] string? name, [FromQuery] string? collectionType, [FromQuery] string[] paths, - [FromBody] LibraryOptionsDto? libraryOptionsDto, + [FromBody] AddVirtualFolderDto? libraryOptionsDto, [FromQuery] bool refreshLibrary = false) { var libraryOptions = libraryOptionsDto?.LibraryOptions ?? new LibraryOptions(); @@ -312,19 +312,17 @@ namespace Jellyfin.Api.Controllers /// <summary> /// Update library options. /// </summary> - /// <param name="id">The library name.</param> - /// <param name="libraryOptions">The library options.</param> + /// <param name="request">The library name and options.</param> /// <response code="204">Library updated.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> [HttpPost("LibraryOptions")] [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult UpdateLibraryOptions( - [FromQuery] string? id, - [FromBody] LibraryOptions? libraryOptions) + [FromBody] UpdateLibraryOptionsDto request) { - var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id); + var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(request.Id); - collectionFolder.UpdateLibraryOptions(libraryOptions); + collectionFolder.UpdateLibraryOptions(request.LibraryOptions); return NoContent(); } } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index 148d8a18e..7fcfc749d 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -10,6 +10,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; @@ -181,7 +182,7 @@ namespace Jellyfin.Api.Controllers DtoOptions dtoOptions, RecommendationType type) { - var itemTypes = new List<string> { nameof(MediaBrowser.Controller.Entities.Movies.Movie) }; + var itemTypes = new List<string> { nameof(Movie) }; if (_serverConfigurationManager.Configuration.EnableExternalContentInSuggestions) { itemTypes.Add(nameof(Trailer)); diff --git a/Jellyfin.Api/Controllers/QuickConnectController.cs b/Jellyfin.Api/Controllers/QuickConnectController.cs new file mode 100644 index 000000000..73da2f906 --- /dev/null +++ b/Jellyfin.Api/Controllers/QuickConnectController.cs @@ -0,0 +1,154 @@ +using System.ComponentModel.DataAnnotations; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.QuickConnect; +using MediaBrowser.Model.QuickConnect; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Quick connect controller. + /// </summary> + public class QuickConnectController : BaseJellyfinApiController + { + private readonly IQuickConnect _quickConnect; + + /// <summary> + /// Initializes a new instance of the <see cref="QuickConnectController"/> class. + /// </summary> + /// <param name="quickConnect">Instance of the <see cref="IQuickConnect"/> interface.</param> + public QuickConnectController(IQuickConnect quickConnect) + { + _quickConnect = quickConnect; + } + + /// <summary> + /// Gets the current quick connect state. + /// </summary> + /// <response code="200">Quick connect state returned.</response> + /// <returns>The current <see cref="QuickConnectState"/>.</returns> + [HttpGet("Status")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<QuickConnectState> GetStatus() + { + _quickConnect.ExpireRequests(); + return _quickConnect.State; + } + + /// <summary> + /// Initiate a new quick connect request. + /// </summary> + /// <response code="200">Quick connect request successfully created.</response> + /// <response code="401">Quick connect is not active on this server.</response> + /// <returns>A <see cref="QuickConnectResult"/> with a secret and code for future use or an error message.</returns> + [HttpGet("Initiate")] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<QuickConnectResult> Initiate() + { + return _quickConnect.TryConnect(); + } + + /// <summary> + /// Attempts to retrieve authentication information. + /// </summary> + /// <param name="secret">Secret previously returned from the Initiate endpoint.</param> + /// <response code="200">Quick connect result returned.</response> + /// <response code="404">Unknown quick connect secret.</response> + /// <returns>An updated <see cref="QuickConnectResult"/>.</returns> + [HttpGet("Connect")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<QuickConnectResult> Connect([FromQuery, Required] string secret) + { + try + { + return _quickConnect.CheckRequestStatus(secret); + } + catch (ResourceNotFoundException) + { + return NotFound("Unknown secret"); + } + } + + /// <summary> + /// Temporarily activates quick connect for five minutes. + /// </summary> + /// <response code="204">Quick connect has been temporarily activated.</response> + /// <response code="403">Quick connect is unavailable on this server.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("Activate")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult Activate() + { + if (_quickConnect.State == QuickConnectState.Unavailable) + { + return Forbid("Quick connect is unavailable"); + } + + _quickConnect.Activate(); + return NoContent(); + } + + /// <summary> + /// Enables or disables quick connect. + /// </summary> + /// <param name="status">New <see cref="QuickConnectState"/>.</param> + /// <response code="204">Quick connect state set successfully.</response> + /// <returns>An <see cref="NoContentResult"/> on success.</returns> + [HttpPost("Available")] + [Authorize(Policy = Policies.RequiresElevation)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public ActionResult Available([FromQuery] QuickConnectState status = QuickConnectState.Available) + { + _quickConnect.SetState(status); + return NoContent(); + } + + /// <summary> + /// Authorizes a pending quick connect request. + /// </summary> + /// <param name="code">Quick connect code to authorize.</param> + /// <response code="200">Quick connect result authorized successfully.</response> + /// <response code="403">Unknown user id.</response> + /// <returns>Boolean indicating if the authorization was successful.</returns> + [HttpPost("Authorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult<bool> Authorize([FromQuery, Required] string code) + { + var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); + if (!userId.HasValue) + { + return Forbid("Unknown user id"); + } + + return _quickConnect.AuthorizeRequest(userId.Value, code); + } + + /// <summary> + /// Deauthorize all quick connect devices for the current user. + /// </summary> + /// <response code="200">All quick connect devices were deleted.</response> + /// <returns>The number of devices that were deleted.</returns> + [HttpPost("Deauthorize")] + [Authorize(Policy = Policies.DefaultAuthorization)] + [ProducesResponseType(StatusCodes.Status200OK)] + public ActionResult<int> Deauthorize() + { + var userId = ClaimHelpers.GetUserId(Request.HttpContext.User); + if (!userId.HasValue) + { + return 0; + } + + return _quickConnect.DeleteAllDevices(userId.Value); + } + } +} diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index 272312522..d67f82219 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -217,6 +217,40 @@ namespace Jellyfin.Api.Controllers } /// <summary> + /// Authenticates a user with quick connect. + /// </summary> + /// <param name="request">The <see cref="QuickConnectDto"/> request.</param> + /// <response code="200">User authenticated.</response> + /// <response code="400">Missing token.</response> + /// <returns>A <see cref="Task"/> containing an <see cref="AuthenticationRequest"/> with information about the new session.</returns> + [HttpPost("AuthenticateWithQuickConnect")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task<ActionResult<AuthenticationResult>> AuthenticateWithQuickConnect([FromBody, Required] QuickConnectDto request) + { + var auth = _authContext.GetAuthorizationInfo(Request); + + try + { + var authRequest = new AuthenticationRequest + { + App = auth.Client, + AppVersion = auth.Version, + DeviceId = auth.DeviceId, + DeviceName = auth.Device, + }; + + return await _sessionManager.AuthenticateQuickConnect( + authRequest, + request.Token).ConfigureAwait(false); + } + catch (SecurityException e) + { + // rethrow adding IP address to message + throw new SecurityException($"[{HttpContext.Connection.RemoteIpAddress}] {e.Message}", e); + } + } + + /// <summary> /// Updates a user's password. /// </summary> /// <param name="userId">The user id.</param> diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index f42810c94..0978d44e9 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -233,7 +233,7 @@ namespace Jellyfin.Api.Controllers .First(); } - var list = primaryVersion.LinkedAlternateVersions.ToList(); + var alternateVersionsOfPrimary = primaryVersion.LinkedAlternateVersions.ToList(); foreach (var item in items.Where(i => i.Id != primaryVersion.Id)) { @@ -241,17 +241,20 @@ namespace Jellyfin.Api.Controllers await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); - list.Add(new LinkedChild + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase))) { - Path = item.Path, - ItemId = item.Id - }); + alternateVersionsOfPrimary.Add(new LinkedChild + { + Path = item.Path, + ItemId = item.Id + }); + } foreach (var linkedItem in item.LinkedAlternateVersions) { - if (!list.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) + if (!alternateVersionsOfPrimary.Any(i => string.Equals(i.Path, linkedItem.Path, StringComparison.OrdinalIgnoreCase))) { - list.Add(linkedItem); + alternateVersionsOfPrimary.Add(linkedItem); } } @@ -262,7 +265,7 @@ namespace Jellyfin.Api.Controllers } } - primaryVersion.LinkedAlternateVersions = list.ToArray(); + primaryVersion.LinkedAlternateVersions = alternateVersionsOfPrimary.ToArray(); await primaryVersion.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false); return NoContent(); } diff --git a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs index 432df9708..e00ed3304 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs +++ b/Jellyfin.Api/Helpers/ProgressiveFileCopier.cs @@ -130,34 +130,10 @@ namespace Jellyfin.Api.Helpers private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken) { var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize); - int bytesRead; - int totalBytesRead = 0; - - if (readAsync) - { - bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); - } - else - { - bytesRead = source.Read(array, 0, array.Length); - } - - while (bytesRead != 0) + try { - var bytesToWrite = bytesRead; - - if (bytesToWrite > 0) - { - await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); - - _bytesWritten += bytesRead; - totalBytesRead += bytesRead; - - if (_job != null) - { - _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); - } - } + int bytesRead; + int totalBytesRead = 0; if (readAsync) { @@ -167,9 +143,40 @@ namespace Jellyfin.Api.Helpers { bytesRead = source.Read(array, 0, array.Length); } - } - return totalBytesRead; + while (bytesRead != 0) + { + var bytesToWrite = bytesRead; + + if (bytesToWrite > 0) + { + await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); + + _bytesWritten += bytesRead; + totalBytesRead += bytesRead; + + if (_job != null) + { + _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); + } + } + + if (readAsync) + { + bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); + } + else + { + bytesRead = source.Read(array, 0, array.Length); + } + } + + return totalBytesRead; + } + finally + { + ArrayPool<byte>.Shared.Return(array); + } } } } diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 24bc07b66..ca0542b03 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -14,9 +14,9 @@ <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" /> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.6" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="3.1.7" /> <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="5.5.1" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs index a13cb90db..ab68d5223 100644 --- a/Jellyfin.Api/Models/LibraryStructureDto/LibraryOptionsDto.cs +++ b/Jellyfin.Api/Models/LibraryStructureDto/AddVirtualFolderDto.cs @@ -3,13 +3,13 @@ namespace Jellyfin.Api.Models.LibraryStructureDto { /// <summary> - /// Library options dto. + /// Add virtual folder dto. /// </summary> - public class LibraryOptionsDto + public class AddVirtualFolderDto { /// <summary> /// Gets or sets library options. /// </summary> public LibraryOptions? LibraryOptions { get; set; } } -}
\ No newline at end of file +} diff --git a/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs new file mode 100644 index 000000000..c78ed51f7 --- /dev/null +++ b/Jellyfin.Api/Models/LibraryStructureDto/UpdateLibraryOptionsDto.cs @@ -0,0 +1,21 @@ +using System; +using MediaBrowser.Model.Configuration; + +namespace Jellyfin.Api.Models.LibraryStructureDto +{ + /// <summary> + /// Update library options dto. + /// </summary> + public class UpdateLibraryOptionsDto + { + /// <summary> + /// Gets or sets the library item id. + /// </summary> + public Guid Id { get; set; } + + /// <summary> + /// Gets or sets library options. + /// </summary> + public LibraryOptions? LibraryOptions { get; set; } + } +} diff --git a/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs new file mode 100644 index 000000000..c3a2d5cec --- /dev/null +++ b/Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Api.Models.UserDtos +{ + /// <summary> + /// The quick connect request body. + /// </summary> + public class QuickConnectDto + { + /// <summary> + /// Gets or sets the quick connect token. + /// </summary> + [Required] + public string? Token { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/ActivityLog.cs b/Jellyfin.Data/Entities/ActivityLog.cs index ac61b9e3b..3858916d1 100644 --- a/Jellyfin.Data/Entities/ActivityLog.cs +++ b/Jellyfin.Data/Entities/ActivityLog.cs @@ -3,6 +3,7 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; using Microsoft.Extensions.Logging; namespace Jellyfin.Data.Entities @@ -10,7 +11,7 @@ namespace Jellyfin.Data.Entities /// <summary> /// An entity referencing an activity log entry. /// </summary> - public partial class ActivityLog : ISavingChanges + public partial class ActivityLog : IHasConcurrencyToken { /// <summary> /// Initializes a new instance of the <see cref="ActivityLog"/> class. diff --git a/Jellyfin.Data/Entities/Artwork.cs b/Jellyfin.Data/Entities/Artwork.cs deleted file mode 100644 index 4508f5488..000000000 --- a/Jellyfin.Data/Entities/Artwork.cs +++ /dev/null @@ -1,210 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class Artwork - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Artwork() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Artwork CreateArtworkUnsafe() - { - return new Artwork(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="path"></param> - /// <param name="kind"></param> - /// <param name="_metadata0"></param> - /// <param name="_personrole1"></param> - public Artwork(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - this.Kind = kind; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Artwork.Add(this); - - if (_personrole1 == null) - { - throw new ArgumentNullException(nameof(_personrole1)); - } - - _personrole1.Artwork = this; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="path"></param> - /// <param name="kind"></param> - /// <param name="_metadata0"></param> - /// <param name="_personrole1"></param> - public static Artwork Create(string path, Enums.ArtKind kind, Metadata _metadata0, PersonRole _personrole1) - { - return new Artwork(path, kind, _metadata0, _personrole1); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Path. - /// </summary> - protected string _Path; - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before setting. - /// </summary> - partial void SetPath(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before returning. - /// </summary> - partial void GetPath(ref string result); - - /// <summary> - /// Required, Max length = 65535 - /// </summary> - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// <summary> - /// Backing field for Kind. - /// </summary> - internal Enums.ArtKind _Kind; - /// <summary> - /// When provided in a partial class, allows value of Kind to be changed before setting. - /// </summary> - partial void SetKind(Enums.ArtKind oldValue, ref Enums.ArtKind newValue); - /// <summary> - /// When provided in a partial class, allows value of Kind to be changed before returning. - /// </summary> - partial void GetKind(ref Enums.ArtKind result); - - /// <summary> - /// Indexed, Required. - /// </summary> - [Required] - public Enums.ArtKind Kind - { - get - { - Enums.ArtKind value = _Kind; - GetKind(ref value); - return _Kind = value; - } - - set - { - Enums.ArtKind oldValue = _Kind; - SetKind(oldValue, ref value); - if (oldValue != value) - { - _Kind = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Book.cs b/Jellyfin.Data/Entities/Book.cs deleted file mode 100644 index b6198ee01..000000000 --- a/Jellyfin.Data/Entities/Book.cs +++ /dev/null @@ -1,72 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Book : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Book() - { - BookMetadata = new HashSet<BookMetadata>(); - Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Book CreateBookUnsafe() - { - return new Book(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public Book(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.BookMetadata = new HashSet<BookMetadata>(); - this.Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static Book Create(Guid urlid, DateTime dateadded) - { - return new Book(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("BookMetadata_BookMetadata_Id")] - public virtual ICollection<BookMetadata> BookMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/BookMetadata.cs b/Jellyfin.Data/Entities/BookMetadata.cs deleted file mode 100644 index 9734cf20e..000000000 --- a/Jellyfin.Data/Entities/BookMetadata.cs +++ /dev/null @@ -1,125 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class BookMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected BookMetadata() - { - Publishers = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static BookMetadata CreateBookMetadataUnsafe() - { - return new BookMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_book0"></param> - public BookMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_book0 == null) - { - throw new ArgumentNullException(nameof(_book0)); - } - - _book0.BookMetadata.Add(this); - - this.Publishers = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_book0"></param> - public static BookMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Book _book0) - { - return new BookMetadata(title, language, dateadded, datemodified, _book0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for ISBN. - /// </summary> - protected long? _ISBN; - /// <summary> - /// When provided in a partial class, allows value of ISBN to be changed before setting. - /// </summary> - partial void SetISBN(long? oldValue, ref long? newValue); - /// <summary> - /// When provided in a partial class, allows value of ISBN to be changed before returning. - /// </summary> - partial void GetISBN(ref long? result); - - public long? ISBN - { - get - { - long? value = _ISBN; - GetISBN(ref value); - return _ISBN = value; - } - - set - { - long? oldValue = _ISBN; - SetISBN(oldValue, ref value); - if (oldValue != value) - { - _ISBN = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Company_Publishers_Id")] - public virtual ICollection<Company> Publishers { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Chapter.cs b/Jellyfin.Data/Entities/Chapter.cs deleted file mode 100644 index 52cdeef78..000000000 --- a/Jellyfin.Data/Entities/Chapter.cs +++ /dev/null @@ -1,277 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Chapter - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Chapter() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Chapter CreateChapterUnsafe() - { - return new Chapter(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="timestart"></param> - /// <param name="_release0"></param> - public Chapter(string language, long timestart, Release _release0) - { - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - this.TimeStart = timestart; - - if (_release0 == null) - { - throw new ArgumentNullException(nameof(_release0)); - } - - _release0.Chapters.Add(this); - - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="timestart"></param> - /// <param name="_release0"></param> - public static Chapter Create(string language, long timestart, Release _release0) - { - return new Chapter(language, timestart, _release0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Backing field for Language. - /// </summary> - protected string _Language; - /// <summary> - /// When provided in a partial class, allows value of Language to be changed before setting. - /// </summary> - partial void SetLanguage(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Language to be changed before returning. - /// </summary> - partial void GetLanguage(ref string result); - - /// <summary> - /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes. - /// </summary> - [Required] - [MinLength(3)] - [MaxLength(3)] - [StringLength(3)] - public string Language - { - get - { - string value = _Language; - GetLanguage(ref value); - return _Language = value; - } - - set - { - string oldValue = _Language; - SetLanguage(oldValue, ref value); - if (oldValue != value) - { - _Language = value; - } - } - } - - /// <summary> - /// Backing field for TimeStart. - /// </summary> - protected long _TimeStart; - /// <summary> - /// When provided in a partial class, allows value of TimeStart to be changed before setting. - /// </summary> - partial void SetTimeStart(long oldValue, ref long newValue); - /// <summary> - /// When provided in a partial class, allows value of TimeStart to be changed before returning. - /// </summary> - partial void GetTimeStart(ref long result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public long TimeStart - { - get - { - long value = _TimeStart; - GetTimeStart(ref value); - return _TimeStart = value; - } - - set - { - long oldValue = _TimeStart; - SetTimeStart(oldValue, ref value); - if (oldValue != value) - { - _TimeStart = value; - } - } - } - - /// <summary> - /// Backing field for TimeEnd. - /// </summary> - protected long? _TimeEnd; - /// <summary> - /// When provided in a partial class, allows value of TimeEnd to be changed before setting. - /// </summary> - partial void SetTimeEnd(long? oldValue, ref long? newValue); - /// <summary> - /// When provided in a partial class, allows value of TimeEnd to be changed before returning. - /// </summary> - partial void GetTimeEnd(ref long? result); - - public long? TimeEnd - { - get - { - long? value = _TimeEnd; - GetTimeEnd(ref value); - return _TimeEnd = value; - } - - set - { - long? oldValue = _TimeEnd; - SetTimeEnd(oldValue, ref value); - if (oldValue != value) - { - _TimeEnd = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Collection.cs b/Jellyfin.Data/Entities/Collection.cs deleted file mode 100644 index 0c317d71e..000000000 --- a/Jellyfin.Data/Entities/Collection.cs +++ /dev/null @@ -1,123 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Collection - { - partial void Init(); - - /// <summary> - /// Default constructor. - /// </summary> - public Collection() - { - CollectionItem = new LinkedList<CollectionItem>(); - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CollectionItem_CollectionItem_Id")] - public virtual ICollection<CollectionItem> CollectionItem { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/CollectionItem.cs b/Jellyfin.Data/Entities/CollectionItem.cs deleted file mode 100644 index fb589c2ba..000000000 --- a/Jellyfin.Data/Entities/CollectionItem.cs +++ /dev/null @@ -1,156 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class CollectionItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected CollectionItem() - { - // NOTE: This class has one-to-one associations with CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static CollectionItem CreateCollectionItemUnsafe() - { - return new CollectionItem(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="_collection0"></param> - /// <param name="_collectionitem1"></param> - /// <param name="_collectionitem2"></param> - public CollectionItem(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) - { - // NOTE: This class has one-to-one associations with CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - if (_collection0 == null) - { - throw new ArgumentNullException(nameof(_collection0)); - } - - _collection0.CollectionItem.Add(this); - - if (_collectionitem1 == null) - { - throw new ArgumentNullException(nameof(_collectionitem1)); - } - - _collectionitem1.Next = this; - - if (_collectionitem2 == null) - { - throw new ArgumentNullException(nameof(_collectionitem2)); - } - - _collectionitem2.Previous = this; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="_collection0"></param> - /// <param name="_collectionitem1"></param> - /// <param name="_collectionitem2"></param> - public static CollectionItem Create(Collection _collection0, CollectionItem _collectionitem1, CollectionItem _collectionitem2) - { - return new CollectionItem(_collection0, _collectionitem1, _collectionitem2); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// Required. - /// </summary> - [ForeignKey("LibraryItem_Id")] - public virtual LibraryItem LibraryItem { get; set; } - - /// <remarks> - /// TODO check if this properly updated dependant and has the proper principal relationship - /// </remarks> - [ForeignKey("CollectionItem_Next_Id")] - public virtual CollectionItem Next { get; set; } - - /// <remarks> - /// TODO check if this properly updated dependant and has the proper principal relationship - /// </remarks> - [ForeignKey("CollectionItem_Previous_Id")] - public virtual CollectionItem Previous { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Company.cs b/Jellyfin.Data/Entities/Company.cs deleted file mode 100644 index 8bd48045d..000000000 --- a/Jellyfin.Data/Entities/Company.cs +++ /dev/null @@ -1,159 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Company - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Company() - { - CompanyMetadata = new HashSet<CompanyMetadata>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Company CreateCompanyUnsafe() - { - return new Company(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="_moviemetadata0"></param> - /// <param name="_seriesmetadata1"></param> - /// <param name="_musicalbummetadata2"></param> - /// <param name="_bookmetadata3"></param> - /// <param name="_company4"></param> - public Company(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) - { - if (_moviemetadata0 == null) - { - throw new ArgumentNullException(nameof(_moviemetadata0)); - } - - _moviemetadata0.Studios.Add(this); - - if (_seriesmetadata1 == null) - { - throw new ArgumentNullException(nameof(_seriesmetadata1)); - } - - _seriesmetadata1.Networks.Add(this); - - if (_musicalbummetadata2 == null) - { - throw new ArgumentNullException(nameof(_musicalbummetadata2)); - } - - _musicalbummetadata2.Labels.Add(this); - - if (_bookmetadata3 == null) - { - throw new ArgumentNullException(nameof(_bookmetadata3)); - } - - _bookmetadata3.Publishers.Add(this); - - if (_company4 == null) - { - throw new ArgumentNullException(nameof(_company4)); - } - - _company4.Parent = this; - - this.CompanyMetadata = new HashSet<CompanyMetadata>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="_moviemetadata0"></param> - /// <param name="_seriesmetadata1"></param> - /// <param name="_musicalbummetadata2"></param> - /// <param name="_bookmetadata3"></param> - /// <param name="_company4"></param> - public static Company Create(MovieMetadata _moviemetadata0, SeriesMetadata _seriesmetadata1, MusicAlbumMetadata _musicalbummetadata2, BookMetadata _bookmetadata3, Company _company4) - { - return new Company(_moviemetadata0, _seriesmetadata1, _musicalbummetadata2, _bookmetadata3, _company4); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CompanyMetadata_CompanyMetadata_Id")] - public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; } - [ForeignKey("Company_Parent_Id")] - public virtual Company Parent { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/CompanyMetadata.cs b/Jellyfin.Data/Entities/CompanyMetadata.cs deleted file mode 100644 index 48ea4bdc5..000000000 --- a/Jellyfin.Data/Entities/CompanyMetadata.cs +++ /dev/null @@ -1,236 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class CompanyMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected CompanyMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static CompanyMetadata CreateCompanyMetadataUnsafe() - { - return new CompanyMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_company0"></param> - public CompanyMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_company0 == null) - { - throw new ArgumentNullException(nameof(_company0)); - } - - _company0.CompanyMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_company0"></param> - public static CompanyMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Company _company0) - { - return new CompanyMetadata(title, language, dateadded, datemodified, _company0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Description. - /// </summary> - protected string _Description; - /// <summary> - /// When provided in a partial class, allows value of Description to be changed before setting. - /// </summary> - partial void SetDescription(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Description to be changed before returning. - /// </summary> - partial void GetDescription(ref string result); - - /// <summary> - /// Max length = 65535 - /// </summary> - [MaxLength(65535)] - [StringLength(65535)] - public string Description - { - get - { - string value = _Description; - GetDescription(ref value); - return _Description = value; - } - - set - { - string oldValue = _Description; - SetDescription(oldValue, ref value); - if (oldValue != value) - { - _Description = value; - } - } - } - - /// <summary> - /// Backing field for Headquarters. - /// </summary> - protected string _Headquarters; - /// <summary> - /// When provided in a partial class, allows value of Headquarters to be changed before setting. - /// </summary> - partial void SetHeadquarters(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Headquarters to be changed before returning. - /// </summary> - partial void GetHeadquarters(ref string result); - - /// <summary> - /// Max length = 255 - /// </summary> - [MaxLength(255)] - [StringLength(255)] - public string Headquarters - { - get - { - string value = _Headquarters; - GetHeadquarters(ref value); - return _Headquarters = value; - } - - set - { - string oldValue = _Headquarters; - SetHeadquarters(oldValue, ref value); - if (oldValue != value) - { - _Headquarters = value; - } - } - } - - /// <summary> - /// Backing field for Country. - /// </summary> - protected string _Country; - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before setting. - /// </summary> - partial void SetCountry(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before returning. - /// </summary> - partial void GetCountry(ref string result); - - /// <summary> - /// Max length = 2 - /// </summary> - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /// <summary> - /// Backing field for Homepage. - /// </summary> - protected string _Homepage; - /// <summary> - /// When provided in a partial class, allows value of Homepage to be changed before setting. - /// </summary> - partial void SetHomepage(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Homepage to be changed before returning. - /// </summary> - partial void GetHomepage(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Homepage - { - get - { - string value = _Homepage; - GetHomepage(ref value); - return _Homepage = value; - } - - set - { - string oldValue = _Homepage; - SetHomepage(oldValue, ref value); - if (oldValue != value) - { - _Homepage = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/CustomItem.cs b/Jellyfin.Data/Entities/CustomItem.cs deleted file mode 100644 index 8ea08488f..000000000 --- a/Jellyfin.Data/Entities/CustomItem.cs +++ /dev/null @@ -1,71 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class CustomItem : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected CustomItem() - { - CustomItemMetadata = new HashSet<CustomItemMetadata>(); - Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static CustomItem CreateCustomItemUnsafe() - { - return new CustomItem(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public CustomItem(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.CustomItemMetadata = new HashSet<CustomItemMetadata>(); - this.Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static CustomItem Create(Guid urlid, DateTime dateadded) - { - return new CustomItem(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("CustomItemMetadata_CustomItemMetadata_Id")] - public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/CustomItemMetadata.cs b/Jellyfin.Data/Entities/CustomItemMetadata.cs deleted file mode 100644 index 9c89399e6..000000000 --- a/Jellyfin.Data/Entities/CustomItemMetadata.cs +++ /dev/null @@ -1,83 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Jellyfin.Data.Entities -{ - public partial class CustomItemMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected CustomItemMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static CustomItemMetadata CreateCustomItemMetadataUnsafe() - { - return new CustomItemMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_customitem0"></param> - public CustomItemMetadata(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_customitem0 == null) - { - throw new ArgumentNullException(nameof(_customitem0)); - } - - _customitem0.CustomItemMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_customitem0"></param> - public static CustomItemMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, CustomItem _customitem0) - { - return new CustomItemMetadata(title, language, dateadded, datemodified, _customitem0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Episode.cs b/Jellyfin.Data/Entities/Episode.cs deleted file mode 100644 index 1c1894448..000000000 --- a/Jellyfin.Data/Entities/Episode.cs +++ /dev/null @@ -1,118 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Episode : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Episode() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Releases = new HashSet<Release>(); - EpisodeMetadata = new HashSet<EpisodeMetadata>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Episode CreateEpisodeUnsafe() - { - return new Episode(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_season0"></param> - public Episode(Guid urlid, DateTime dateadded, Season _season0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_season0 == null) - { - throw new ArgumentNullException(nameof(_season0)); - } - - _season0.Episodes.Add(this); - - this.Releases = new HashSet<Release>(); - this.EpisodeMetadata = new HashSet<EpisodeMetadata>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_season0"></param> - public static Episode Create(Guid urlid, DateTime dateadded, Season _season0) - { - return new Episode(urlid, dateadded, _season0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for EpisodeNumber. - /// </summary> - protected int? _EpisodeNumber; - /// <summary> - /// When provided in a partial class, allows value of EpisodeNumber to be changed before setting. - /// </summary> - partial void SetEpisodeNumber(int? oldValue, ref int? newValue); - /// <summary> - /// When provided in a partial class, allows value of EpisodeNumber to be changed before returning. - /// </summary> - partial void GetEpisodeNumber(ref int? result); - - public int? EpisodeNumber - { - get - { - int? value = _EpisodeNumber; - GetEpisodeNumber(ref value); - return _EpisodeNumber = value; - } - - set - { - int? oldValue = _EpisodeNumber; - SetEpisodeNumber(oldValue, ref value); - if (oldValue != value) - { - _EpisodeNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - [ForeignKey("EpisodeMetadata_EpisodeMetadata_Id")] - public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/EpisodeMetadata.cs b/Jellyfin.Data/Entities/EpisodeMetadata.cs deleted file mode 100644 index 26ad7200b..000000000 --- a/Jellyfin.Data/Entities/EpisodeMetadata.cs +++ /dev/null @@ -1,198 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; - -namespace Jellyfin.Data.Entities -{ - public partial class EpisodeMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected EpisodeMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static EpisodeMetadata CreateEpisodeMetadataUnsafe() - { - return new EpisodeMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_episode0"></param> - public EpisodeMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_episode0 == null) - { - throw new ArgumentNullException(nameof(_episode0)); - } - - _episode0.EpisodeMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_episode0"></param> - public static EpisodeMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Episode _episode0) - { - return new EpisodeMetadata(title, language, dateadded, datemodified, _episode0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Outline. - /// </summary> - protected string _Outline; - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// </summary> - partial void SetOutline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// </summary> - partial void GetOutline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// <summary> - /// Backing field for Plot. - /// </summary> - protected string _Plot; - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// </summary> - partial void SetPlot(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// </summary> - partial void GetPlot(ref string result); - - /// <summary> - /// Max length = 65535 - /// </summary> - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// <summary> - /// Backing field for Tagline. - /// </summary> - protected string _Tagline; - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// </summary> - partial void SetTagline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// </summary> - partial void GetTagline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Genre.cs b/Jellyfin.Data/Entities/Genre.cs deleted file mode 100644 index 43a180f6b..000000000 --- a/Jellyfin.Data/Entities/Genre.cs +++ /dev/null @@ -1,162 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Genre - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Genre() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Genre CreateGenreUnsafe() - { - return new Genre(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="name"></param> - /// <param name="_metadata0"></param> - public Genre(string name, Metadata _metadata0) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Genres.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="name"></param> - /// <param name="_metadata0"></param> - public static Genre Create(string name, Metadata _metadata0) - { - return new Genre(name, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - internal string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Indexed, Required, Max length = 255 - /// </summary> - [Required] - [MaxLength(255)] - [StringLength(255)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Group.cs b/Jellyfin.Data/Entities/Group.cs index a1ec6b1fa..ca12ba421 100644 --- a/Jellyfin.Data/Entities/Group.cs +++ b/Jellyfin.Data/Entities/Group.cs @@ -6,13 +6,14 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// <summary> /// An entity representing a group. /// </summary> - public partial class Group : IHasPermissions, ISavingChanges + public partial class Group : IHasPermissions, IHasConcurrencyToken { /// <summary> /// Initializes a new instance of the <see cref="Group"/> class. diff --git a/Jellyfin.Data/Entities/Libraries/Artwork.cs b/Jellyfin.Data/Entities/Libraries/Artwork.cs new file mode 100644 index 000000000..7be22af25 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Artwork.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing artwork. + /// </summary> + public class Artwork : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Artwork"/> class. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="kind">The kind of art.</param> + /// <param name="owner">The owner.</param> + public Artwork(string path, ArtKind kind, IHasArtwork owner) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + Path = path; + Kind = kind; + + owner?.Artwork.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Artwork"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Artwork() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the path. + /// </summary> + /// <remarks> + /// Required, Max length = 65535. + /// </remarks> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path { get; set; } + + /// <summary> + /// Gets or sets the kind of artwork. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public ArtKind Kind { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Book.cs b/Jellyfin.Data/Entities/Libraries/Book.cs new file mode 100644 index 000000000..8337788dd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Book.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a book. + /// </summary> + public class Book : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="Book"/> class. + /// </summary> + public Book() + { + BookMetadata = new HashSet<BookMetadata>(); + Releases = new HashSet<Release>(); + } + + /// <summary> + /// Gets or sets a collection containing the metadata for this book. + /// </summary> + public virtual ICollection<BookMetadata> BookMetadata { get; protected set; } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/BookMetadata.cs b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs new file mode 100644 index 000000000..bd716712b --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/BookMetadata.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity containing metadata for a book. + /// </summary> + public class BookMetadata : Metadata, IHasCompanies + { + /// <summary> + /// Initializes a new instance of the <see cref="BookMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="book">The book.</param> + public BookMetadata(string title, string language, Book book) : base(title, language) + { + if (book == null) + { + throw new ArgumentNullException(nameof(book)); + } + + book.BookMetadata.Add(this); + + Publishers = new HashSet<Company>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="BookMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected BookMetadata() + { + } + + /// <summary> + /// Gets or sets the ISBN. + /// </summary> + public long? Isbn { get; set; } + + /// <summary> + /// Gets or sets a collection of the publishers for this book. + /// </summary> + public virtual ICollection<Company> Publishers { get; protected set; } + + /// <inheritdoc /> + [NotMapped] + public ICollection<Company> Companies => Publishers; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Chapter.cs b/Jellyfin.Data/Entities/Libraries/Chapter.cs new file mode 100644 index 000000000..d9293c3cc --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Chapter.cs @@ -0,0 +1,102 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a chapter. + /// </summary> + public class Chapter : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Chapter"/> class. + /// </summary> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="startTime">The start time for this chapter.</param> + /// <param name="release">The release.</param> + public Chapter(string language, long startTime, Release release) + { + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException(nameof(language)); + } + + Language = language; + StartTime = startTime; + + if (release == null) + { + throw new ArgumentNullException(nameof(release)); + } + + release.Chapters.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Chapter"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Chapter() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the language. + /// </summary> + /// <remarks> + /// Required, Min length = 3, Max length = 3 + /// ISO-639-3 3-character language codes. + /// </remarks> + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language { get; set; } + + /// <summary> + /// Gets or sets the start time. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public long StartTime { get; set; } + + /// <summary> + /// Gets or sets the end time. + /// </summary> + public long? EndTime { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Collection.cs b/Jellyfin.Data/Entities/Libraries/Collection.cs new file mode 100644 index 000000000..2e1bbcfb6 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Collection.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a collection. + /// </summary> + public class Collection : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Collection"/> class. + /// </summary> + public Collection() + { + Items = new HashSet<CollectionItem>(); + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a collection containing this collection's items. + /// </summary> + public virtual ICollection<CollectionItem> Items { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CollectionItem.cs b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs new file mode 100644 index 000000000..c9306f630 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CollectionItem.cs @@ -0,0 +1,94 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a collection item. + /// </summary> + public class CollectionItem : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="CollectionItem"/> class. + /// </summary> + /// <param name="collection">The collection.</param> + /// <param name="previous">The previous item.</param> + /// <param name="next">The next item.</param> + public CollectionItem(Collection collection, CollectionItem previous, CollectionItem next) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + collection.Items.Add(this); + + if (next != null) + { + Next = next; + next.Previous = this; + } + + if (previous != null) + { + Previous = previous; + previous.Next = this; + } + } + + /// <summary> + /// Initializes a new instance of the <see cref="CollectionItem"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected CollectionItem() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets the library item. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public virtual LibraryItem LibraryItem { get; set; } + + /// <summary> + /// Gets or sets the next item in the collection. + /// </summary> + /// <remarks> + /// TODO check if this properly updated dependant and has the proper principal relationship + /// </remarks> + public virtual CollectionItem Next { get; set; } + + /// <summary> + /// Gets or sets the previous item in the collection. + /// </summary> + /// <remarks> + /// TODO check if this properly updated dependant and has the proper principal relationship + /// </remarks> + public virtual CollectionItem Previous { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Company.cs b/Jellyfin.Data/Entities/Libraries/Company.cs new file mode 100644 index 000000000..02da26bc2 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Company.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a company. + /// </summary> + public class Company : IHasCompanies, IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Company"/> class. + /// </summary> + /// <param name="owner">The owner of this company.</param> + public Company(IHasCompanies owner) + { + owner?.Companies.Add(this); + + CompanyMetadata = new HashSet<CompanyMetadata>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Company"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Company() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a collection containing the metadata. + /// </summary> + public virtual ICollection<CompanyMetadata> CompanyMetadata { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing this company's child companies. + /// </summary> + public virtual ICollection<Company> ChildCompanies { get; protected set; } + + /// <inheritdoc /> + [NotMapped] + public ICollection<Company> Companies => ChildCompanies; + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs new file mode 100644 index 000000000..60cc96a34 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CompanyMetadata.cs @@ -0,0 +1,74 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity holding metadata for a <see cref="Company"/>. + /// </summary> + public class CompanyMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="CompanyMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="company">The company.</param> + public CompanyMetadata(string title, string language, Company company) : base(title, language) + { + if (company == null) + { + throw new ArgumentNullException(nameof(company)); + } + + company.CompanyMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="CompanyMetadata"/> class. + /// </summary> + protected CompanyMetadata() + { + } + + /// <summary> + /// Gets or sets the description. + /// </summary> + /// <remarks> + /// Max length = 65535. + /// </remarks> + [MaxLength(65535)] + [StringLength(65535)] + public string Description { get; set; } + + /// <summary> + /// Gets or sets the headquarters. + /// </summary> + /// <remarks> + /// Max length = 255. + /// </remarks> + [MaxLength(255)] + [StringLength(255)] + public string Headquarters { get; set; } + + /// <summary> + /// Gets or sets the country code. + /// </summary> + /// <remarks> + /// Max length = 2. + /// </remarks> + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets the homepage. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Homepage { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CustomItem.cs b/Jellyfin.Data/Entities/Libraries/CustomItem.cs new file mode 100644 index 000000000..6a4f0a537 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CustomItem.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a custom item. + /// </summary> + public class CustomItem : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="CustomItem"/> class. + /// </summary> + public CustomItem() + { + CustomItemMetadata = new HashSet<CustomItemMetadata>(); + Releases = new HashSet<Release>(); + } + + /// <summary> + /// Gets or sets a collection containing the metadata for this item. + /// </summary> + public virtual ICollection<CustomItemMetadata> CustomItemMetadata { get; protected set; } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs new file mode 100644 index 000000000..bc1835528 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/CustomItemMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity containing metadata for a custom item. + /// </summary> + public class CustomItemMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="item">The item.</param> + public CustomItemMetadata(string title, string language, CustomItem item) : base(title, language) + { + if (item == null) + { + throw new ArgumentNullException(nameof(item)); + } + + item.CustomItemMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="CustomItemMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected CustomItemMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Episode.cs b/Jellyfin.Data/Entities/Libraries/Episode.cs new file mode 100644 index 000000000..430a11e3c --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Episode.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing an episode. + /// </summary> + public class Episode : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="Episode"/> class. + /// </summary> + /// <param name="season">The season.</param> + public Episode(Season season) + { + if (season == null) + { + throw new ArgumentNullException(nameof(season)); + } + + season.Episodes.Add(this); + + Releases = new HashSet<Release>(); + EpisodeMetadata = new HashSet<EpisodeMetadata>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Episode"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Episode() + { + } + + /// <summary> + /// Gets or sets the episode number. + /// </summary> + public int? EpisodeNumber { get; set; } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the metadata for this episode. + /// </summary> + public virtual ICollection<EpisodeMetadata> EpisodeMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs new file mode 100644 index 000000000..348100cb4 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/EpisodeMetadata.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity containing metadata for an <see cref="Episode"/>. + /// </summary> + public class EpisodeMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="episode">The episode.</param> + public EpisodeMetadata(string title, string language, Episode episode) : base(title, language) + { + if (episode == null) + { + throw new ArgumentNullException(nameof(episode)); + } + + episode.EpisodeMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected EpisodeMetadata() + { + } + + /// <summary> + /// Gets or sets the outline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// <summary> + /// Gets or sets the plot. + /// </summary> + /// <remarks> + /// Max length = 65535. + /// </remarks> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// <summary> + /// Gets or sets the tagline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Genre.cs b/Jellyfin.Data/Entities/Libraries/Genre.cs new file mode 100644 index 000000000..aeedd7bfd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Genre.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a genre. + /// </summary> + public class Genre : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Genre"/> class. + /// </summary> + /// <param name="name">The name.</param> + /// <param name="metadata">The metadata.</param> + public Genre(string name, Metadata metadata) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Genres.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Genre"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Genre() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Indexed, Required, Max length = 255. + /// </remarks> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string Name { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Library.cs b/Jellyfin.Data/Entities/Libraries/Library.cs new file mode 100644 index 000000000..4f82a2e2a --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Library.cs @@ -0,0 +1,76 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a library. + /// </summary> + public class Library : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Library"/> class. + /// </summary> + /// <param name="name">The name of the library.</param> + public Library(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + } + + /// <summary> + /// Initializes a new instance of the <see cref="Library"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Library() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Required, Max length = 128. + /// </remarks> + [Required] + [MaxLength(128)] + [StringLength(128)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the root path of the library. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + [Required] + public string Path { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/LibraryItem.cs b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs new file mode 100644 index 000000000..a9167aa7f --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/LibraryItem.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a library item. + /// </summary> + public abstract class LibraryItem : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="LibraryItem"/> class. + /// </summary> + /// <param name="library">The library of this item.</param> + protected LibraryItem(Library library) + { + DateAdded = DateTime.UtcNow; + Library = library; + } + + /// <summary> + /// Initializes a new instance of the <see cref="LibraryItem"/> class. + /// </summary> + protected LibraryItem() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the date this library item was added. + /// </summary> + public DateTime DateAdded { get; protected set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// <summary> + /// Gets or sets the library of this item. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + [Required] + public virtual Library Library { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MediaFile.cs b/Jellyfin.Data/Entities/Libraries/MediaFile.cs new file mode 100644 index 000000000..8bc649c98 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MediaFile.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a file on disk. + /// </summary> + public class MediaFile : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="MediaFile"/> class. + /// </summary> + /// <param name="path">The path relative to the LibraryRoot.</param> + /// <param name="kind">The file kind.</param> + /// <param name="release">The release.</param> + public MediaFile(string path, MediaFileKind kind, Release release) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(nameof(path)); + } + + Path = path; + Kind = kind; + + if (release == null) + { + throw new ArgumentNullException(nameof(release)); + } + + release.MediaFiles.Add(this); + + MediaFileStreams = new HashSet<MediaFileStream>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MediaFile"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MediaFile() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the path relative to the library root. + /// </summary> + /// <remarks> + /// Required, Max length = 65535. + /// </remarks> + [Required] + [MaxLength(65535)] + [StringLength(65535)] + public string Path { get; set; } + + /// <summary> + /// Gets or sets the kind of media file. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public MediaFileKind Kind { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a collection containing the streams in this file. + /// </summary> + public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs new file mode 100644 index 000000000..5b03e260e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MediaFileStream.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a stream in a media file. + /// </summary> + public class MediaFileStream : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="MediaFileStream"/> class. + /// </summary> + /// <param name="streamNumber">The number of this stream.</param> + /// <param name="mediaFile">The media file.</param> + public MediaFileStream(int streamNumber, MediaFile mediaFile) + { + StreamNumber = streamNumber; + + if (mediaFile == null) + { + throw new ArgumentNullException(nameof(mediaFile)); + } + + mediaFile.MediaFileStreams.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MediaFileStream"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MediaFileStream() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the stream number. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public int StreamNumber { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Metadata.cs b/Jellyfin.Data/Entities/Libraries/Metadata.cs new file mode 100644 index 000000000..877bb5fbd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Metadata.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An abstract class that holds metadata. + /// </summary> + public abstract class Metadata : IHasArtwork, IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Metadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + protected Metadata(string title, string language) + { + if (string.IsNullOrEmpty(title)) + { + throw new ArgumentNullException(nameof(title)); + } + + if (string.IsNullOrEmpty(language)) + { + throw new ArgumentNullException(nameof(language)); + } + + Title = title; + Language = language; + DateAdded = DateTime.UtcNow; + DateModified = DateAdded; + + PersonRoles = new HashSet<PersonRole>(); + Genres = new HashSet<Genre>(); + Artwork = new HashSet<Artwork>(); + Ratings = new HashSet<Rating>(); + Sources = new HashSet<MetadataProviderId>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Metadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to being abstract. + /// </remarks> + protected Metadata() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the title. + /// </summary> + /// <remarks> + /// Required, Max length = 1024. + /// </remarks> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Title { get; set; } + + /// <summary> + /// Gets or sets the original title. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string OriginalTitle { get; set; } + + /// <summary> + /// Gets or sets the sort title. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string SortTitle { get; set; } + + /// <summary> + /// Gets or sets the language. + /// </summary> + /// <remarks> + /// Required, Min length = 3, Max length = 3. + /// ISO-639-3 3-character language codes. + /// </remarks> + [Required] + [MinLength(3)] + [MaxLength(3)] + [StringLength(3)] + public string Language { get; set; } + + /// <summary> + /// Gets or sets the release date. + /// </summary> + public DateTimeOffset? ReleaseDate { get; set; } + + /// <summary> + /// Gets or sets the date added. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public DateTime DateAdded { get; protected set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public DateTime DateModified { get; set; } + + /// <summary> + /// Gets or sets the row version. + /// </summary> + /// <remarks> + /// Required, ConcurrencyToken. + /// </remarks> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a collection containing the person roles for this item. + /// </summary> + public virtual ICollection<PersonRole> PersonRoles { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the generes for this item. + /// </summary> + public virtual ICollection<Genre> Genres { get; protected set; } + + /// <inheritdoc /> + public virtual ICollection<Artwork> Artwork { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the ratings for this item. + /// </summary> + public virtual ICollection<Rating> Ratings { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the metadata sources for this item. + /// </summary> + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs new file mode 100644 index 000000000..a18a612bc --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MetadataProvider.cs @@ -0,0 +1,67 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a metadata provider. + /// </summary> + public class MetadataProvider : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="MetadataProvider"/> class. + /// </summary> + /// <param name="name">The name of the metadata provider.</param> + public MetadataProvider(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + } + + /// <summary> + /// Initializes a new instance of the <see cref="MetadataProvider"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MetadataProvider() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Required, Max length = 1024. + /// </remarks> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs new file mode 100644 index 000000000..6e6de598e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MetadataProviderId.cs @@ -0,0 +1,83 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a unique identifier for a metadata provider. + /// </summary> + public class MetadataProviderId : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="MetadataProviderId"/> class. + /// </summary> + /// <param name="providerId">The provider id.</param> + /// <param name="metadata">The metadata entity.</param> + public MetadataProviderId(string providerId, Metadata metadata) + { + if (string.IsNullOrEmpty(providerId)) + { + throw new ArgumentNullException(nameof(providerId)); + } + + ProviderId = providerId; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Sources.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MetadataProviderId"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MetadataProviderId() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the provider id. + /// </summary> + /// <remarks> + /// Required, Max length = 255. + /// </remarks> + [Required] + [MaxLength(255)] + [StringLength(255)] + public string ProviderId { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets the metadata provider. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public virtual MetadataProvider MetadataProvider { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Movie.cs b/Jellyfin.Data/Entities/Libraries/Movie.cs new file mode 100644 index 000000000..0a8cc83dd --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Movie.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a movie. + /// </summary> + public class Movie : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="Movie"/> class. + /// </summary> + public Movie() + { + Releases = new HashSet<Release>(); + MovieMetadata = new HashSet<MovieMetadata>(); + } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the metadata for this movie. + /// </summary> + public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs new file mode 100644 index 000000000..31102bf13 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MovieMetadata.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity holding the metadata for a movie. + /// </summary> + public class MovieMetadata : Metadata, IHasCompanies + { + /// <summary> + /// Initializes a new instance of the <see cref="MovieMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the movie.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="movie">The movie.</param> + public MovieMetadata(string title, string language, Movie movie) : base(title, language) + { + Studios = new HashSet<Company>(); + + movie.MovieMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MovieMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MovieMetadata() + { + } + + /// <summary> + /// Gets or sets the outline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// <summary> + /// Gets or sets the tagline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + + /// <summary> + /// Gets or sets the plot. + /// </summary> + /// <remarks> + /// Max length = 65535. + /// </remarks> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// <summary> + /// Gets or sets the country code. + /// </summary> + /// <remarks> + /// Max length = 2. + /// </remarks> + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets the studios that produced this movie. + /// </summary> + public virtual ICollection<Company> Studios { get; protected set; } + + /// <inheritdoc /> + [NotMapped] + public ICollection<Company> Companies => Studios; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs new file mode 100644 index 000000000..2ed1f78c5 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbum.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a music album. + /// </summary> + public class MusicAlbum : LibraryItem + { + /// <summary> + /// Initializes a new instance of the <see cref="MusicAlbum"/> class. + /// </summary> + public MusicAlbum() + { + MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); + Tracks = new HashSet<Track>(); + } + + /// <summary> + /// Gets or sets a collection containing the album metadata. + /// </summary> + public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the tracks. + /// </summary> + public virtual ICollection<Track> Tracks { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs new file mode 100644 index 000000000..cc5919bfe --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/MusicAlbumMetadata.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity holding the metadata for a music album. + /// </summary> + public class MusicAlbumMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the album.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="album">The music album.</param> + public MusicAlbumMetadata(string title, string language, MusicAlbum album) : base(title, language) + { + Labels = new HashSet<Company>(); + + album.MusicAlbumMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="MusicAlbumMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected MusicAlbumMetadata() + { + } + + /// <summary> + /// Gets or sets the barcode. + /// </summary> + /// <remarks> + /// Max length = 255. + /// </remarks> + [MaxLength(255)] + [StringLength(255)] + public string Barcode { get; set; } + + /// <summary> + /// Gets or sets the label number. + /// </summary> + /// <remarks> + /// Max length = 255. + /// </remarks> + [MaxLength(255)] + [StringLength(255)] + public string LabelNumber { get; set; } + + /// <summary> + /// Gets or sets the country code. + /// </summary> + /// <remarks> + /// Max length = 2. + /// </remarks> + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets a collection containing the labels. + /// </summary> + public virtual ICollection<Company> Labels { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Person.cs b/Jellyfin.Data/Entities/Libraries/Person.cs new file mode 100644 index 000000000..8beb3dd08 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Person.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a person. + /// </summary> + public class Person : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Person"/> class. + /// </summary> + /// <param name="name">The name of the person.</param> + public Person(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + DateAdded = DateTime.UtcNow; + DateModified = DateAdded; + + Sources = new HashSet<MetadataProviderId>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Person"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Person() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Required, Max length = 1024. + /// </remarks> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the source id. + /// </summary> + /// <remarks> + /// Max length = 255. + /// </remarks> + [MaxLength(256)] + [StringLength(256)] + public string SourceId { get; set; } + + /// <summary> + /// Gets or sets the date added. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public DateTime DateAdded { get; protected set; } + + /// <summary> + /// Gets or sets the date modified. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public DateTime DateModified { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a list of metadata sources for this person. + /// </summary> + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/PersonRole.cs b/Jellyfin.Data/Entities/Libraries/PersonRole.cs new file mode 100644 index 000000000..5290228d6 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/PersonRole.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a person's role in media. + /// </summary> + public class PersonRole : IHasArtwork, IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="PersonRole"/> class. + /// </summary> + /// <param name="type">The role type.</param> + /// <param name="metadata">The metadata.</param> + public PersonRole(PersonRoleType type, Metadata metadata) + { + Type = type; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.PersonRoles.Add(this); + + Sources = new HashSet<MetadataProviderId>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="PersonRole"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected PersonRole() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name of the person's role. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Role { get; set; } + + /// <summary> + /// Gets or sets the person's role type. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public PersonRoleType Type { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; protected set; } + + /// <summary> + /// Gets or sets the person. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + [Required] + public virtual Person Person { get; set; } + + /// <inheritdoc /> + public virtual ICollection<Artwork> Artwork { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the metadata sources for this person role. + /// </summary> + public virtual ICollection<MetadataProviderId> Sources { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Photo.cs b/Jellyfin.Data/Entities/Libraries/Photo.cs new file mode 100644 index 000000000..44338a4ce --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Photo.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a photo. + /// </summary> + public class Photo : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="Photo"/> class. + /// </summary> + public Photo() + { + PhotoMetadata = new HashSet<PhotoMetadata>(); + Releases = new HashSet<Release>(); + } + + /// <summary> + /// Gets or sets a collection containing the photo metadata. + /// </summary> + public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs new file mode 100644 index 000000000..1ef9dd5f9 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/PhotoMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity that holds metadata for a photo. + /// </summary> + public class PhotoMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="PhotoMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the photo.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="photo">The photo.</param> + public PhotoMetadata(string title, string language, Photo photo) : base(title, language) + { + if (photo == null) + { + throw new ArgumentNullException(nameof(photo)); + } + + photo.PhotoMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="PhotoMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected PhotoMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Rating.cs b/Jellyfin.Data/Entities/Libraries/Rating.cs new file mode 100644 index 000000000..ba054a39e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Rating.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a rating for an entity. + /// </summary> + public class Rating : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Rating"/> class. + /// </summary> + /// <param name="value">The value.</param> + /// <param name="metadata">The metadata.</param> + public Rating(double value, Metadata metadata) + { + Value = value; + + if (metadata == null) + { + throw new ArgumentNullException(nameof(metadata)); + } + + metadata.Ratings.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Rating"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Rating() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the value. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public double Value { get; set; } + + /// <summary> + /// Gets or sets the number of votes. + /// </summary> + public int? Votes { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets the rating type. + /// If this is <c>null</c> it's the internal user rating. + /// </summary> + public virtual RatingSource RatingType { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/RatingSource.cs b/Jellyfin.Data/Entities/Libraries/RatingSource.cs new file mode 100644 index 000000000..549f41804 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/RatingSource.cs @@ -0,0 +1,92 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// This is the entity to store review ratings, not age ratings. + /// </summary> + public class RatingSource : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="RatingSource"/> class. + /// </summary> + /// <param name="minimumValue">The minimum value.</param> + /// <param name="maximumValue">The maximum value.</param> + /// <param name="rating">The rating.</param> + public RatingSource(double minimumValue, double maximumValue, Rating rating) + { + MinimumValue = minimumValue; + MaximumValue = maximumValue; + + if (rating == null) + { + throw new ArgumentNullException(nameof(rating)); + } + + rating.RatingType = this; + } + + /// <summary> + /// Initializes a new instance of the <see cref="RatingSource"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected RatingSource() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <summary> + /// Gets or sets the minimum value. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public double MinimumValue { get; set; } + + /// <summary> + /// Gets or sets the maximum value. + /// </summary> + /// <remarks> + /// Required. + /// </remarks> + public double MaximumValue { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets the metadata source. + /// </summary> + public virtual MetadataProviderId Source { get; set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Release.cs b/Jellyfin.Data/Entities/Libraries/Release.cs new file mode 100644 index 000000000..43c7080d7 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Release.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a release for a library item, eg. Director's cut vs. standard. + /// </summary> + public class Release : IHasConcurrencyToken + { + /// <summary> + /// Initializes a new instance of the <see cref="Release"/> class. + /// </summary> + /// <param name="name">The name of this release.</param> + /// <param name="owner">The owner of this release.</param> + public Release(string name, IHasReleases owner) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + + owner?.Releases.Add(this); + + MediaFiles = new HashSet<MediaFile>(); + Chapters = new HashSet<Chapter>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Release"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Release() + { + } + + /// <summary> + /// Gets or sets the id. + /// </summary> + /// <remarks> + /// Identity, Indexed, Required. + /// </remarks> + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; protected set; } + + /// <summary> + /// Gets or sets the name. + /// </summary> + /// <remarks> + /// Required, Max length = 1024. + /// </remarks> + [Required] + [MaxLength(1024)] + [StringLength(1024)] + public string Name { get; set; } + + /// <inheritdoc /> + [ConcurrencyCheck] + public uint RowVersion { get; set; } + + /// <summary> + /// Gets or sets a collection containing the media files for this release. + /// </summary> + public virtual ICollection<MediaFile> MediaFiles { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the chapters for this release. + /// </summary> + public virtual ICollection<Chapter> Chapters { get; protected set; } + + /// <inheritdoc /> + public void OnSavingChanges() + { + RowVersion++; + } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Season.cs b/Jellyfin.Data/Entities/Libraries/Season.cs new file mode 100644 index 000000000..eef788bad --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Season.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a season. + /// </summary> + public class Season : LibraryItem + { + /// <summary> + /// Initializes a new instance of the <see cref="Season"/> class. + /// </summary> + /// <param name="series">The series.</param> + public Season(Series series) + { + if (series == null) + { + throw new ArgumentNullException(nameof(series)); + } + + series.Seasons.Add(this); + + Episodes = new HashSet<Episode>(); + SeasonMetadata = new HashSet<SeasonMetadata>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Season"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Season() + { + } + + /// <summary> + /// Gets or sets the season number. + /// </summary> + public int? SeasonNumber { get; set; } + + /// <summary> + /// Gets or sets the season metadata. + /// </summary> + public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the number of episodes. + /// </summary> + public virtual ICollection<Episode> Episodes { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs new file mode 100644 index 000000000..eedeb089e --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/SeasonMetadata.cs @@ -0,0 +1,47 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity that holds metadata for seasons. + /// </summary> + public class SeasonMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="SeasonMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="season">The season.</param> + public SeasonMetadata(string title, string language, Season season) : base(title, language) + { + if (season == null) + { + throw new ArgumentNullException(nameof(season)); + } + + season.SeasonMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="SeasonMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected SeasonMetadata() + { + } + + /// <summary> + /// Gets or sets the outline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Series.cs b/Jellyfin.Data/Entities/Libraries/Series.cs new file mode 100644 index 000000000..e959c1fe0 --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Series.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a a series. + /// </summary> + public class Series : LibraryItem + { + /// <summary> + /// Initializes a new instance of the <see cref="Series"/> class. + /// </summary> + public Series() + { + DateAdded = DateTime.UtcNow; + Seasons = new HashSet<Season>(); + SeriesMetadata = new HashSet<SeriesMetadata>(); + } + + /// <summary> + /// Gets or sets the days of week. + /// </summary> + public DayOfWeek? AirsDayOfWeek { get; set; } + + /// <summary> + /// Gets or sets the time the show airs, ignore the date portion. + /// </summary> + public DateTimeOffset? AirsTime { get; set; } + + /// <summary> + /// Gets or sets the date the series first aired. + /// </summary> + public DateTime? FirstAired { get; set; } + + /// <summary> + /// Gets or sets a collection containing the series metadata. + /// </summary> + public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the seasons. + /// </summary> + public virtual ICollection<Season> Seasons { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs new file mode 100644 index 000000000..898f3006d --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/SeriesMetadata.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing series metadata. + /// </summary> + public class SeriesMetadata : Metadata, IHasCompanies + { + /// <summary> + /// Initializes a new instance of the <see cref="SeriesMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="series">The series.</param> + public SeriesMetadata(string title, string language, Series series) : base(title, language) + { + if (series == null) + { + throw new ArgumentNullException(nameof(series)); + } + + series.SeriesMetadata.Add(this); + + Networks = new HashSet<Company>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="SeriesMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected SeriesMetadata() + { + } + + /// <summary> + /// Gets or sets the outline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Outline { get; set; } + + /// <summary> + /// Gets or sets the plot. + /// </summary> + /// <remarks> + /// Max length = 65535. + /// </remarks> + [MaxLength(65535)] + [StringLength(65535)] + public string Plot { get; set; } + + /// <summary> + /// Gets or sets the tagline. + /// </summary> + /// <remarks> + /// Max length = 1024. + /// </remarks> + [MaxLength(1024)] + [StringLength(1024)] + public string Tagline { get; set; } + + /// <summary> + /// Gets or sets the country code. + /// </summary> + /// <remarks> + /// Max length = 2. + /// </remarks> + [MaxLength(2)] + [StringLength(2)] + public string Country { get; set; } + + /// <summary> + /// Gets or sets a collection containing the networks. + /// </summary> + public virtual ICollection<Company> Networks { get; protected set; } + + /// <inheritdoc /> + [NotMapped] + public ICollection<Company> Companies => Networks; + } +} diff --git a/Jellyfin.Data/Entities/Libraries/Track.cs b/Jellyfin.Data/Entities/Libraries/Track.cs new file mode 100644 index 000000000..09ce82a9b --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/Track.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using Jellyfin.Data.Interfaces; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity representing a track. + /// </summary> + public class Track : LibraryItem, IHasReleases + { + /// <summary> + /// Initializes a new instance of the <see cref="Track"/> class. + /// </summary> + /// <param name="album">The album.</param> + public Track(MusicAlbum album) + { + if (album == null) + { + throw new ArgumentNullException(nameof(album)); + } + + album.Tracks.Add(this); + + Releases = new HashSet<Release>(); + TrackMetadata = new HashSet<TrackMetadata>(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="Track"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected Track() + { + } + + /// <summary> + /// Gets or sets the track number. + /// </summary> + public int? TrackNumber { get; set; } + + /// <inheritdoc /> + public virtual ICollection<Release> Releases { get; protected set; } + + /// <summary> + /// Gets or sets a collection containing the track metadata. + /// </summary> + public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; } + } +} diff --git a/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs new file mode 100644 index 000000000..048068a1a --- /dev/null +++ b/Jellyfin.Data/Entities/Libraries/TrackMetadata.cs @@ -0,0 +1,36 @@ +using System; + +namespace Jellyfin.Data.Entities.Libraries +{ + /// <summary> + /// An entity holding metadata for a track. + /// </summary> + public class TrackMetadata : Metadata + { + /// <summary> + /// Initializes a new instance of the <see cref="TrackMetadata"/> class. + /// </summary> + /// <param name="title">The title or name of the object.</param> + /// <param name="language">ISO-639-3 3-character language codes.</param> + /// <param name="track">The track.</param> + public TrackMetadata(string title, string language, Track track) : base(title, language) + { + if (track == null) + { + throw new ArgumentNullException(nameof(track)); + } + + track.TrackMetadata.Add(this); + } + + /// <summary> + /// Initializes a new instance of the <see cref="TrackMetadata"/> class. + /// </summary> + /// <remarks> + /// Default constructor. Protected due to required properties, but present because EF needs it. + /// </remarks> + protected TrackMetadata() + { + } + } +} diff --git a/Jellyfin.Data/Entities/Library.cs b/Jellyfin.Data/Entities/Library.cs deleted file mode 100644 index 23cc9bd7d..000000000 --- a/Jellyfin.Data/Entities/Library.cs +++ /dev/null @@ -1,153 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Library - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Library() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Library CreateLibraryUnsafe() - { - return new Library(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="name"></param> - public Library(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="name"></param> - public static Library Create(string name) - { - return new Library(name); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Required, Max length = 1024 - /// </summary> - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/LibraryItem.cs b/Jellyfin.Data/Entities/LibraryItem.cs deleted file mode 100644 index 00b2f9497..000000000 --- a/Jellyfin.Data/Entities/LibraryItem.cs +++ /dev/null @@ -1,175 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public abstract partial class LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to being abstract. - /// </summary> - protected LibraryItem() - { - Init(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - protected LibraryItem(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for UrlId. - /// </summary> - internal Guid _UrlId; - /// <summary> - /// When provided in a partial class, allows value of UrlId to be changed before setting. - /// </summary> - partial void SetUrlId(Guid oldValue, ref Guid newValue); - /// <summary> - /// When provided in a partial class, allows value of UrlId to be changed before returning. - /// </summary> - partial void GetUrlId(ref Guid result); - - /// <summary> - /// Indexed, Required - /// This is whats gets displayed in the Urls and API requests. This could also be a string. - /// </summary> - [Required] - public Guid UrlId - { - get - { - Guid value = _UrlId; - GetUrlId(ref value); - return _UrlId = value; - } - - set - { - Guid oldValue = _UrlId; - SetUrlId(oldValue, ref value); - if (oldValue != value) - { - _UrlId = value; - } - } - } - - /// <summary> - /// Backing field for DateAdded. - /// </summary> - protected DateTime _DateAdded; - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// </summary> - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// </summary> - partial void GetDateAdded(ref DateTime result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// Required. - /// </summary> - [ForeignKey("LibraryRoot_Id")] - public virtual LibraryRoot LibraryRoot { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/LibraryRoot.cs b/Jellyfin.Data/Entities/LibraryRoot.cs deleted file mode 100644 index 07e16fff4..000000000 --- a/Jellyfin.Data/Entities/LibraryRoot.cs +++ /dev/null @@ -1,199 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class LibraryRoot - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected LibraryRoot() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static LibraryRoot CreateLibraryRootUnsafe() - { - return new LibraryRoot(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="path">Absolute Path.</param> - public LibraryRoot(string path) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="path">Absolute Path.</param> - public static LibraryRoot Create(string path) - { - return new LibraryRoot(path); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Path. - /// </summary> - protected string _Path; - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before setting. - /// </summary> - partial void SetPath(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before returning. - /// </summary> - partial void GetPath(ref string result); - - /// <summary> - /// Required, Max length = 65535 - /// Absolute Path. - /// </summary> - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// <summary> - /// Backing field for NetworkPath. - /// </summary> - protected string _NetworkPath; - /// <summary> - /// When provided in a partial class, allows value of NetworkPath to be changed before setting. - /// </summary> - partial void SetNetworkPath(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of NetworkPath to be changed before returning. - /// </summary> - partial void GetNetworkPath(ref string result); - - /// <summary> - /// Max length = 65535 - /// Absolute network path, for example for transcoding sattelites. - /// </summary> - [MaxLength(65535)] - [StringLength(65535)] - public string NetworkPath - { - get - { - string value = _NetworkPath; - GetNetworkPath(ref value); - return _NetworkPath = value; - } - - set - { - string oldValue = _NetworkPath; - SetNetworkPath(oldValue, ref value); - if (oldValue != value) - { - _NetworkPath = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// Required. - /// </summary> - [ForeignKey("Library_Id")] - public virtual Library Library { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/MediaFile.cs b/Jellyfin.Data/Entities/MediaFile.cs deleted file mode 100644 index b69dbe2fa..000000000 --- a/Jellyfin.Data/Entities/MediaFile.cs +++ /dev/null @@ -1,212 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MediaFile - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MediaFile() - { - MediaFileStreams = new HashSet<MediaFileStream>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MediaFile CreateMediaFileUnsafe() - { - return new MediaFile(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="path">Relative to the LibraryRoot.</param> - /// <param name="kind"></param> - /// <param name="_release0"></param> - public MediaFile(string path, Enums.MediaFileKind kind, Release _release0) - { - if (string.IsNullOrEmpty(path)) - { - throw new ArgumentNullException(nameof(path)); - } - - this.Path = path; - - this.Kind = kind; - - if (_release0 == null) - { - throw new ArgumentNullException(nameof(_release0)); - } - - _release0.MediaFiles.Add(this); - - this.MediaFileStreams = new HashSet<MediaFileStream>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="path">Relative to the LibraryRoot.</param> - /// <param name="kind"></param> - /// <param name="_release0"></param> - public static MediaFile Create(string path, Enums.MediaFileKind kind, Release _release0) - { - return new MediaFile(path, kind, _release0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Path. - /// </summary> - protected string _Path; - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before setting. - /// </summary> - partial void SetPath(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Path to be changed before returning. - /// </summary> - partial void GetPath(ref string result); - - /// <summary> - /// Required, Max length = 65535 - /// Relative to the LibraryRoot. - /// </summary> - [Required] - [MaxLength(65535)] - [StringLength(65535)] - public string Path - { - get - { - string value = _Path; - GetPath(ref value); - return _Path = value; - } - - set - { - string oldValue = _Path; - SetPath(oldValue, ref value); - if (oldValue != value) - { - _Path = value; - } - } - } - - /// <summary> - /// Backing field for Kind. - /// </summary> - protected Enums.MediaFileKind _Kind; - /// <summary> - /// When provided in a partial class, allows value of Kind to be changed before setting. - /// </summary> - partial void SetKind(Enums.MediaFileKind oldValue, ref Enums.MediaFileKind newValue); - /// <summary> - /// When provided in a partial class, allows value of Kind to be changed before returning. - /// </summary> - partial void GetKind(ref Enums.MediaFileKind result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public Enums.MediaFileKind Kind - { - get - { - Enums.MediaFileKind value = _Kind; - GetKind(ref value); - return _Kind = value; - } - - set - { - Enums.MediaFileKind oldValue = _Kind; - SetKind(oldValue, ref value); - if (oldValue != value) - { - _Kind = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("MediaFileStream_MediaFileStreams_Id")] - public virtual ICollection<MediaFileStream> MediaFileStreams { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MediaFileStream.cs b/Jellyfin.Data/Entities/MediaFileStream.cs deleted file mode 100644 index 1c59e663d..000000000 --- a/Jellyfin.Data/Entities/MediaFileStream.cs +++ /dev/null @@ -1,155 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MediaFileStream - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MediaFileStream() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MediaFileStream CreateMediaFileStreamUnsafe() - { - return new MediaFileStream(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="streamnumber"></param> - /// <param name="_mediafile0"></param> - public MediaFileStream(int streamnumber, MediaFile _mediafile0) - { - this.StreamNumber = streamnumber; - - if (_mediafile0 == null) - { - throw new ArgumentNullException(nameof(_mediafile0)); - } - - _mediafile0.MediaFileStreams.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="streamnumber"></param> - /// <param name="_mediafile0"></param> - public static MediaFileStream Create(int streamnumber, MediaFile _mediafile0) - { - return new MediaFileStream(streamnumber, _mediafile0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for StreamNumber. - /// </summary> - protected int _StreamNumber; - /// <summary> - /// When provided in a partial class, allows value of StreamNumber to be changed before setting. - /// </summary> - partial void SetStreamNumber(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of StreamNumber to be changed before returning. - /// </summary> - partial void GetStreamNumber(ref int result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public int StreamNumber - { - get - { - int value = _StreamNumber; - GetStreamNumber(ref value); - return _StreamNumber = value; - } - - set - { - int oldValue = _StreamNumber; - SetStreamNumber(oldValue, ref value); - if (oldValue != value) - { - _StreamNumber = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Metadata.cs b/Jellyfin.Data/Entities/Metadata.cs deleted file mode 100644 index 42525fa99..000000000 --- a/Jellyfin.Data/Entities/Metadata.cs +++ /dev/null @@ -1,399 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public abstract partial class Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to being abstract. - /// </summary> - protected Metadata() - { - PersonRoles = new HashSet<PersonRole>(); - Genres = new HashSet<Genre>(); - Artwork = new HashSet<Artwork>(); - Ratings = new HashSet<Rating>(); - Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - protected Metadata(string title, string language, DateTime dateadded, DateTime datemodified) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - this.PersonRoles = new HashSet<PersonRole>(); - this.Genres = new HashSet<Genre>(); - this.Artwork = new HashSet<Artwork>(); - this.Ratings = new HashSet<Rating>(); - this.Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Title. - /// </summary> - protected string _Title; - /// <summary> - /// When provided in a partial class, allows value of Title to be changed before setting. - /// </summary> - partial void SetTitle(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Title to be changed before returning. - /// </summary> - partial void GetTitle(ref string result); - - /// <summary> - /// Required, Max length = 1024 - /// The title or name of the object. - /// </summary> - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Title - { - get - { - string value = _Title; - GetTitle(ref value); - return _Title = value; - } - - set - { - string oldValue = _Title; - SetTitle(oldValue, ref value); - if (oldValue != value) - { - _Title = value; - } - } - } - - /// <summary> - /// Backing field for OriginalTitle. - /// </summary> - protected string _OriginalTitle; - /// <summary> - /// When provided in a partial class, allows value of OriginalTitle to be changed before setting. - /// </summary> - partial void SetOriginalTitle(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of OriginalTitle to be changed before returning. - /// </summary> - partial void GetOriginalTitle(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string OriginalTitle - { - get - { - string value = _OriginalTitle; - GetOriginalTitle(ref value); - return _OriginalTitle = value; - } - - set - { - string oldValue = _OriginalTitle; - SetOriginalTitle(oldValue, ref value); - if (oldValue != value) - { - _OriginalTitle = value; - } - } - } - - /// <summary> - /// Backing field for SortTitle. - /// </summary> - protected string _SortTitle; - /// <summary> - /// When provided in a partial class, allows value of SortTitle to be changed before setting. - /// </summary> - partial void SetSortTitle(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of SortTitle to be changed before returning. - /// </summary> - partial void GetSortTitle(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string SortTitle - { - get - { - string value = _SortTitle; - GetSortTitle(ref value); - return _SortTitle = value; - } - - set - { - string oldValue = _SortTitle; - SetSortTitle(oldValue, ref value); - if (oldValue != value) - { - _SortTitle = value; - } - } - } - - /// <summary> - /// Backing field for Language. - /// </summary> - protected string _Language; - /// <summary> - /// When provided in a partial class, allows value of Language to be changed before setting. - /// </summary> - partial void SetLanguage(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Language to be changed before returning. - /// </summary> - partial void GetLanguage(ref string result); - - /// <summary> - /// Required, Min length = 3, Max length = 3 - /// ISO-639-3 3-character language codes. - /// </summary> - [Required] - [MinLength(3)] - [MaxLength(3)] - [StringLength(3)] - public string Language - { - get - { - string value = _Language; - GetLanguage(ref value); - return _Language = value; - } - - set - { - string oldValue = _Language; - SetLanguage(oldValue, ref value); - if (oldValue != value) - { - _Language = value; - } - } - } - - /// <summary> - /// Backing field for ReleaseDate. - /// </summary> - protected DateTimeOffset? _ReleaseDate; - /// <summary> - /// When provided in a partial class, allows value of ReleaseDate to be changed before setting. - /// </summary> - partial void SetReleaseDate(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// <summary> - /// When provided in a partial class, allows value of ReleaseDate to be changed before returning. - /// </summary> - partial void GetReleaseDate(ref DateTimeOffset? result); - - public DateTimeOffset? ReleaseDate - { - get - { - DateTimeOffset? value = _ReleaseDate; - GetReleaseDate(ref value); - return _ReleaseDate = value; - } - - set - { - DateTimeOffset? oldValue = _ReleaseDate; - SetReleaseDate(oldValue, ref value); - if (oldValue != value) - { - _ReleaseDate = value; - } - } - } - - /// <summary> - /// Backing field for DateAdded. - /// </summary> - protected DateTime _DateAdded; - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// </summary> - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// </summary> - partial void GetDateAdded(ref DateTime result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// <summary> - /// Backing field for DateModified. - /// </summary> - protected DateTime _DateModified; - /// <summary> - /// When provided in a partial class, allows value of DateModified to be changed before setting. - /// </summary> - partial void SetDateModified(DateTime oldValue, ref DateTime newValue); - /// <summary> - /// When provided in a partial class, allows value of DateModified to be changed before returning. - /// </summary> - partial void GetDateModified(ref DateTime result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public DateTime DateModified - { - get - { - DateTime value = _DateModified; - GetDateModified(ref value); - return _DateModified = value; - } - - internal set - { - DateTime oldValue = _DateModified; - SetDateModified(oldValue, ref value); - if (oldValue != value) - { - _DateModified = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection<PersonRole> PersonRoles { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection<Genre> Genres { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection<Artwork> Artwork { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection<Rating> Ratings { get; protected set; } - - [ForeignKey("PersonRole_PersonRoles_Id")] - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MetadataProvider.cs b/Jellyfin.Data/Entities/MetadataProvider.cs deleted file mode 100644 index ebb2c1dbc..000000000 --- a/Jellyfin.Data/Entities/MetadataProvider.cs +++ /dev/null @@ -1,153 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MetadataProvider - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MetadataProvider() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MetadataProvider CreateMetadataProviderUnsafe() - { - return new MetadataProvider(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="name"></param> - public MetadataProvider(string name) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="name"></param> - public static MetadataProvider Create(string name) - { - return new MetadataProvider(name); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Required, Max length = 1024 - /// </summary> - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/MetadataProviderId.cs b/Jellyfin.Data/Entities/MetadataProviderId.cs deleted file mode 100644 index ca3e16b1a..000000000 --- a/Jellyfin.Data/Entities/MetadataProviderId.cs +++ /dev/null @@ -1,201 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MetadataProviderId - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MetadataProviderId() - { - // NOTE: This class has one-to-one associations with MetadataProviderId. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MetadataProviderId CreateMetadataProviderIdUnsafe() - { - return new MetadataProviderId(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="providerid"></param> - /// <param name="_metadata0"></param> - /// <param name="_person1"></param> - /// <param name="_personrole2"></param> - /// <param name="_ratingsource3"></param> - public MetadataProviderId(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) - { - // NOTE: This class has one-to-one associations with MetadataProviderId. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - if (string.IsNullOrEmpty(providerid)) - { - throw new ArgumentNullException(nameof(providerid)); - } - - this.ProviderId = providerid; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Sources.Add(this); - - if (_person1 == null) - { - throw new ArgumentNullException(nameof(_person1)); - } - - _person1.Sources.Add(this); - - if (_personrole2 == null) - { - throw new ArgumentNullException(nameof(_personrole2)); - } - - _personrole2.Sources.Add(this); - - if (_ratingsource3 == null) - { - throw new ArgumentNullException(nameof(_ratingsource3)); - } - - _ratingsource3.Source = this; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="providerid"></param> - /// <param name="_metadata0"></param> - /// <param name="_person1"></param> - /// <param name="_personrole2"></param> - /// <param name="_ratingsource3"></param> - public static MetadataProviderId Create(string providerid, Metadata _metadata0, Person _person1, PersonRole _personrole2, RatingSource _ratingsource3) - { - return new MetadataProviderId(providerid, _metadata0, _person1, _personrole2, _ratingsource3); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for ProviderId. - /// </summary> - protected string _ProviderId; - /// <summary> - /// When provided in a partial class, allows value of ProviderId to be changed before setting. - /// </summary> - partial void SetProviderId(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of ProviderId to be changed before returning. - /// </summary> - partial void GetProviderId(ref string result); - - /// <summary> - /// Required, Max length = 255 - /// </summary> - [Required] - [MaxLength(255)] - [StringLength(255)] - public string ProviderId - { - get - { - string value = _ProviderId; - GetProviderId(ref value); - return _ProviderId = value; - } - - set - { - string oldValue = _ProviderId; - SetProviderId(oldValue, ref value); - if (oldValue != value) - { - _ProviderId = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// Required. - /// </summary> - [ForeignKey("MetadataProvider_Id")] - public virtual MetadataProvider MetadataProvider { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Movie.cs b/Jellyfin.Data/Entities/Movie.cs deleted file mode 100644 index 842d5b2b0..000000000 --- a/Jellyfin.Data/Entities/Movie.cs +++ /dev/null @@ -1,72 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Movie : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Movie() - { - Releases = new HashSet<Release>(); - MovieMetadata = new HashSet<MovieMetadata>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Movie CreateMovieUnsafe() - { - return new Movie(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public Movie(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.Releases = new HashSet<Release>(); - this.MovieMetadata = new HashSet<MovieMetadata>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static Movie Create(Guid urlid, DateTime dateadded) - { - return new Movie(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - - [ForeignKey("MovieMetadata_MovieMetadata_Id")] - public virtual ICollection<MovieMetadata> MovieMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MovieMetadata.cs b/Jellyfin.Data/Entities/MovieMetadata.cs deleted file mode 100644 index a6c82dda8..000000000 --- a/Jellyfin.Data/Entities/MovieMetadata.cs +++ /dev/null @@ -1,244 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MovieMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MovieMetadata() - { - Studios = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MovieMetadata CreateMovieMetadataUnsafe() - { - return new MovieMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_movie0"></param> - public MovieMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_movie0 == null) - { - throw new ArgumentNullException(nameof(_movie0)); - } - - _movie0.MovieMetadata.Add(this); - - this.Studios = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_movie0"></param> - public static MovieMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Movie _movie0) - { - return new MovieMetadata(title, language, dateadded, datemodified, _movie0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Outline. - /// </summary> - protected string _Outline; - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// </summary> - partial void SetOutline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// </summary> - partial void GetOutline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// <summary> - /// Backing field for Plot. - /// </summary> - protected string _Plot; - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// </summary> - partial void SetPlot(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// </summary> - partial void GetPlot(ref string result); - - /// <summary> - /// Max length = 65535 - /// </summary> - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// <summary> - /// Backing field for Tagline. - /// </summary> - protected string _Tagline; - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// </summary> - partial void SetTagline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// </summary> - partial void GetTagline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /// <summary> - /// Backing field for Country. - /// </summary> - protected string _Country; - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before setting. - /// </summary> - partial void SetCountry(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before returning. - /// </summary> - partial void GetCountry(ref string result); - - /// <summary> - /// Max length = 2 - /// </summary> - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Company_Studios_Id")] - public virtual ICollection<Company> Studios { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MusicAlbum.cs b/Jellyfin.Data/Entities/MusicAlbum.cs deleted file mode 100644 index e03c3bfb0..000000000 --- a/Jellyfin.Data/Entities/MusicAlbum.cs +++ /dev/null @@ -1,71 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MusicAlbum : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MusicAlbum() - { - MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); - Tracks = new HashSet<Track>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MusicAlbum CreateMusicAlbumUnsafe() - { - return new MusicAlbum(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public MusicAlbum(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.MusicAlbumMetadata = new HashSet<MusicAlbumMetadata>(); - this.Tracks = new HashSet<Track>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static MusicAlbum Create(Guid urlid, DateTime dateadded) - { - return new MusicAlbum(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MusicAlbumMetadata_MusicAlbumMetadata_Id")] - public virtual ICollection<MusicAlbumMetadata> MusicAlbumMetadata { get; protected set; } - - [ForeignKey("Track_Tracks_Id")] - public virtual ICollection<Track> Tracks { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs b/Jellyfin.Data/Entities/MusicAlbumMetadata.cs deleted file mode 100644 index 01ad736ce..000000000 --- a/Jellyfin.Data/Entities/MusicAlbumMetadata.cs +++ /dev/null @@ -1,207 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class MusicAlbumMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected MusicAlbumMetadata() - { - Labels = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static MusicAlbumMetadata CreateMusicAlbumMetadataUnsafe() - { - return new MusicAlbumMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_musicalbum0"></param> - public MusicAlbumMetadata(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_musicalbum0 == null) - { - throw new ArgumentNullException(nameof(_musicalbum0)); - } - - _musicalbum0.MusicAlbumMetadata.Add(this); - - this.Labels = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_musicalbum0"></param> - public static MusicAlbumMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, MusicAlbum _musicalbum0) - { - return new MusicAlbumMetadata(title, language, dateadded, datemodified, _musicalbum0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Barcode. - /// </summary> - protected string _Barcode; - /// <summary> - /// When provided in a partial class, allows value of Barcode to be changed before setting. - /// </summary> - partial void SetBarcode(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Barcode to be changed before returning. - /// </summary> - partial void GetBarcode(ref string result); - - /// <summary> - /// Max length = 255 - /// </summary> - [MaxLength(255)] - [StringLength(255)] - public string Barcode - { - get - { - string value = _Barcode; - GetBarcode(ref value); - return _Barcode = value; - } - - set - { - string oldValue = _Barcode; - SetBarcode(oldValue, ref value); - if (oldValue != value) - { - _Barcode = value; - } - } - } - - /// <summary> - /// Backing field for LabelNumber. - /// </summary> - protected string _LabelNumber; - /// <summary> - /// When provided in a partial class, allows value of LabelNumber to be changed before setting. - /// </summary> - partial void SetLabelNumber(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of LabelNumber to be changed before returning. - /// </summary> - partial void GetLabelNumber(ref string result); - - /// <summary> - /// Max length = 255 - /// </summary> - [MaxLength(255)] - [StringLength(255)] - public string LabelNumber - { - get - { - string value = _LabelNumber; - GetLabelNumber(ref value); - return _LabelNumber = value; - } - - set - { - string oldValue = _LabelNumber; - SetLabelNumber(oldValue, ref value); - if (oldValue != value) - { - _LabelNumber = value; - } - } - } - - /// <summary> - /// Backing field for Country. - /// </summary> - protected string _Country; - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before setting. - /// </summary> - partial void SetCountry(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before returning. - /// </summary> - partial void GetCountry(ref string result); - - /// <summary> - /// Max length = 2 - /// </summary> - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Company_Labels_Id")] - public virtual ICollection<Company> Labels { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Permission.cs b/Jellyfin.Data/Entities/Permission.cs index af3270a88..c0f67f836 100644 --- a/Jellyfin.Data/Entities/Permission.cs +++ b/Jellyfin.Data/Entities/Permission.cs @@ -3,13 +3,14 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// <summary> /// An entity representing whether the associated user has a specific permission. /// </summary> - public partial class Permission : ISavingChanges + public partial class Permission : IHasConcurrencyToken { /// <summary> /// Initializes a new instance of the <see cref="Permission"/> class. diff --git a/Jellyfin.Data/Entities/Person.cs b/Jellyfin.Data/Entities/Person.cs deleted file mode 100644 index f0cfb7322..000000000 --- a/Jellyfin.Data/Entities/Person.cs +++ /dev/null @@ -1,317 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Person - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Person() - { - Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Person CreatePersonUnsafe() - { - return new Person(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid"></param> - /// <param name="name"></param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - public Person(Guid urlid, string name, DateTime dateadded, DateTime datemodified) - { - this.UrlId = urlid; - - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - this.Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid"></param> - /// <param name="name"></param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - public static Person Create(Guid urlid, string name, DateTime dateadded, DateTime datemodified) - { - return new Person(urlid, name, dateadded, datemodified); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for UrlId. - /// </summary> - protected Guid _UrlId; - /// <summary> - /// When provided in a partial class, allows value of UrlId to be changed before setting. - /// </summary> - partial void SetUrlId(Guid oldValue, ref Guid newValue); - /// <summary> - /// When provided in a partial class, allows value of UrlId to be changed before returning. - /// </summary> - partial void GetUrlId(ref Guid result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public Guid UrlId - { - get - { - Guid value = _UrlId; - GetUrlId(ref value); - return _UrlId = value; - } - - set - { - Guid oldValue = _UrlId; - SetUrlId(oldValue, ref value); - if (oldValue != value) - { - _UrlId = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Required, Max length = 1024 - /// </summary> - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Backing field for SourceId. - /// </summary> - protected string _SourceId; - /// <summary> - /// When provided in a partial class, allows value of SourceId to be changed before setting. - /// </summary> - partial void SetSourceId(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of SourceId to be changed before returning. - /// </summary> - partial void GetSourceId(ref string result); - - /// <summary> - /// Max length = 255 - /// </summary> - [MaxLength(255)] - [StringLength(255)] - public string SourceId - { - get - { - string value = _SourceId; - GetSourceId(ref value); - return _SourceId = value; - } - - set - { - string oldValue = _SourceId; - SetSourceId(oldValue, ref value); - if (oldValue != value) - { - _SourceId = value; - } - } - } - - /// <summary> - /// Backing field for DateAdded. - /// </summary> - protected DateTime _DateAdded; - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before setting. - /// </summary> - partial void SetDateAdded(DateTime oldValue, ref DateTime newValue); - /// <summary> - /// When provided in a partial class, allows value of DateAdded to be changed before returning. - /// </summary> - partial void GetDateAdded(ref DateTime result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public DateTime DateAdded - { - get - { - DateTime value = _DateAdded; - GetDateAdded(ref value); - return _DateAdded = value; - } - - internal set - { - DateTime oldValue = _DateAdded; - SetDateAdded(oldValue, ref value); - if (oldValue != value) - { - _DateAdded = value; - } - } - } - - /// <summary> - /// Backing field for DateModified. - /// </summary> - protected DateTime _DateModified; - /// <summary> - /// When provided in a partial class, allows value of DateModified to be changed before setting. - /// </summary> - partial void SetDateModified(DateTime oldValue, ref DateTime newValue); - /// <summary> - /// When provided in a partial class, allows value of DateModified to be changed before returning. - /// </summary> - partial void GetDateModified(ref DateTime result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public DateTime DateModified - { - get - { - DateTime value = _DateModified; - GetDateModified(ref value); - return _DateModified = value; - } - - internal set - { - DateTime oldValue = _DateModified; - SetDateModified(oldValue, ref value); - if (oldValue != value) - { - _DateModified = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MetadataProviderId_Sources_Id")] - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/PersonRole.cs b/Jellyfin.Data/Entities/PersonRole.cs deleted file mode 100644 index 895a9f47a..000000000 --- a/Jellyfin.Data/Entities/PersonRole.cs +++ /dev/null @@ -1,217 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class PersonRole - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected PersonRole() - { - // NOTE: This class has one-to-one associations with PersonRole. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static PersonRole CreatePersonRoleUnsafe() - { - return new PersonRole(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="type"></param> - /// <param name="_metadata0"></param> - public PersonRole(Enums.PersonRoleType type, Metadata _metadata0) - { - // NOTE: This class has one-to-one associations with PersonRole. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.Type = type; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.PersonRoles.Add(this); - - this.Sources = new HashSet<MetadataProviderId>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="type"></param> - /// <param name="_metadata0"></param> - public static PersonRole Create(Enums.PersonRoleType type, Metadata _metadata0) - { - return new PersonRole(type, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Role. - /// </summary> - protected string _Role; - /// <summary> - /// When provided in a partial class, allows value of Role to be changed before setting. - /// </summary> - partial void SetRole(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Role to be changed before returning. - /// </summary> - partial void GetRole(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Role - { - get - { - string value = _Role; - GetRole(ref value); - return _Role = value; - } - - set - { - string oldValue = _Role; - SetRole(oldValue, ref value); - if (oldValue != value) - { - _Role = value; - } - } - } - - /// <summary> - /// Backing field for Type. - /// </summary> - protected Enums.PersonRoleType _Type; - /// <summary> - /// When provided in a partial class, allows value of Type to be changed before setting. - /// </summary> - partial void SetType(Enums.PersonRoleType oldValue, ref Enums.PersonRoleType newValue); - /// <summary> - /// When provided in a partial class, allows value of Type to be changed before returning. - /// </summary> - partial void GetType(ref Enums.PersonRoleType result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public Enums.PersonRoleType Type - { - get - { - Enums.PersonRoleType value = _Type; - GetType(ref value); - return _Type = value; - } - - set - { - Enums.PersonRoleType oldValue = _Type; - SetType(oldValue, ref value); - if (oldValue != value) - { - _Type = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// Required. - /// </summary> - [ForeignKey("Person_Id")] - - public virtual Person Person { get; set; } - - [ForeignKey("Artwork_Artwork_Id")] - public virtual Artwork Artwork { get; set; } - - [ForeignKey("MetadataProviderId_Sources_Id")] - public virtual ICollection<MetadataProviderId> Sources { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Photo.cs b/Jellyfin.Data/Entities/Photo.cs deleted file mode 100644 index 7648bc212..000000000 --- a/Jellyfin.Data/Entities/Photo.cs +++ /dev/null @@ -1,71 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Photo : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Photo() - { - PhotoMetadata = new HashSet<PhotoMetadata>(); - Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Photo CreatePhotoUnsafe() - { - return new Photo(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public Photo(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.PhotoMetadata = new HashSet<PhotoMetadata>(); - this.Releases = new HashSet<Release>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static Photo Create(Guid urlid, DateTime dateadded) - { - return new Photo(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("PhotoMetadata_PhotoMetadata_Id")] - public virtual ICollection<PhotoMetadata> PhotoMetadata { get; protected set; } - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/PhotoMetadata.cs b/Jellyfin.Data/Entities/PhotoMetadata.cs deleted file mode 100644 index 3f06d3f2b..000000000 --- a/Jellyfin.Data/Entities/PhotoMetadata.cs +++ /dev/null @@ -1,84 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class PhotoMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected PhotoMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static PhotoMetadata CreatePhotoMetadataUnsafe() - { - return new PhotoMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_photo0"></param> - public PhotoMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_photo0 == null) - { - throw new ArgumentNullException(nameof(_photo0)); - } - - _photo0.PhotoMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_photo0"></param> - public static PhotoMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Photo _photo0) - { - return new PhotoMetadata(title, language, dateadded, datemodified, _photo0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Preference.cs b/Jellyfin.Data/Entities/Preference.cs index 0ca9d7eff..1797f0a40 100644 --- a/Jellyfin.Data/Entities/Preference.cs +++ b/Jellyfin.Data/Entities/Preference.cs @@ -2,13 +2,14 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// <summary> /// An entity representing a preference attached to a user or group. /// </summary> - public class Preference : ISavingChanges + public class Preference : IHasConcurrencyToken { /// <summary> /// Initializes a new instance of the <see cref="Preference"/> class. diff --git a/Jellyfin.Data/Entities/Rating.cs b/Jellyfin.Data/Entities/Rating.cs deleted file mode 100644 index c57b0a0e8..000000000 --- a/Jellyfin.Data/Entities/Rating.cs +++ /dev/null @@ -1,194 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Rating - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Rating() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Rating CreateRatingUnsafe() - { - return new Rating(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="value"></param> - /// <param name="_metadata0"></param> - public Rating(double value, Metadata _metadata0) - { - this.Value = value; - - if (_metadata0 == null) - { - throw new ArgumentNullException(nameof(_metadata0)); - } - - _metadata0.Ratings.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="value"></param> - /// <param name="_metadata0"></param> - public static Rating Create(double value, Metadata _metadata0) - { - return new Rating(value, _metadata0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Value. - /// </summary> - protected double _Value; - /// <summary> - /// When provided in a partial class, allows value of Value to be changed before setting. - /// </summary> - partial void SetValue(double oldValue, ref double newValue); - /// <summary> - /// When provided in a partial class, allows value of Value to be changed before returning. - /// </summary> - partial void GetValue(ref double result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public double Value - { - get - { - double value = _Value; - GetValue(ref value); - return _Value = value; - } - - set - { - double oldValue = _Value; - SetValue(oldValue, ref value); - if (oldValue != value) - { - _Value = value; - } - } - } - - /// <summary> - /// Backing field for Votes. - /// </summary> - protected int? _Votes; - /// <summary> - /// When provided in a partial class, allows value of Votes to be changed before setting. - /// </summary> - partial void SetVotes(int? oldValue, ref int? newValue); - /// <summary> - /// When provided in a partial class, allows value of Votes to be changed before returning. - /// </summary> - partial void GetVotes(ref int? result); - - public int? Votes - { - get - { - int? value = _Votes; - GetVotes(ref value); - return _Votes = value; - } - - set - { - int? oldValue = _Votes; - SetVotes(oldValue, ref value); - if (oldValue != value) - { - _Votes = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - /// <summary> - /// If this is NULL it's the internal user rating. - /// </summary> - [ForeignKey("RatingSource_RatingType_Id")] - public virtual RatingSource RatingType { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/RatingSource.cs b/Jellyfin.Data/Entities/RatingSource.cs deleted file mode 100644 index 2ea8e3b31..000000000 --- a/Jellyfin.Data/Entities/RatingSource.cs +++ /dev/null @@ -1,239 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - /// <summary> - /// This is the entity to store review ratings, not age ratings. - /// </summary> - public partial class RatingSource - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected RatingSource() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static RatingSource CreateRatingSourceUnsafe() - { - return new RatingSource(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="maximumvalue"></param> - /// <param name="minimumvalue"></param> - /// <param name="_rating0"></param> - public RatingSource(double maximumvalue, double minimumvalue, Rating _rating0) - { - this.MaximumValue = maximumvalue; - - this.MinimumValue = minimumvalue; - - if (_rating0 == null) - { - throw new ArgumentNullException(nameof(_rating0)); - } - - _rating0.RatingType = this; - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="maximumvalue"></param> - /// <param name="minimumvalue"></param> - /// <param name="_rating0"></param> - public static RatingSource Create(double maximumvalue, double minimumvalue, Rating _rating0) - { - return new RatingSource(maximumvalue, minimumvalue, _rating0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Backing field for MaximumValue. - /// </summary> - protected double _MaximumValue; - /// <summary> - /// When provided in a partial class, allows value of MaximumValue to be changed before setting. - /// </summary> - partial void SetMaximumValue(double oldValue, ref double newValue); - /// <summary> - /// When provided in a partial class, allows value of MaximumValue to be changed before returning. - /// </summary> - partial void GetMaximumValue(ref double result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public double MaximumValue - { - get - { - double value = _MaximumValue; - GetMaximumValue(ref value); - return _MaximumValue = value; - } - - set - { - double oldValue = _MaximumValue; - SetMaximumValue(oldValue, ref value); - if (oldValue != value) - { - _MaximumValue = value; - } - } - } - - /// <summary> - /// Backing field for MinimumValue. - /// </summary> - protected double _MinimumValue; - /// <summary> - /// When provided in a partial class, allows value of MinimumValue to be changed before setting. - /// </summary> - partial void SetMinimumValue(double oldValue, ref double newValue); - /// <summary> - /// When provided in a partial class, allows value of MinimumValue to be changed before returning. - /// </summary> - partial void GetMinimumValue(ref double result); - - /// <summary> - /// Required. - /// </summary> - [Required] - public double MinimumValue - { - get - { - double value = _MinimumValue; - GetMinimumValue(ref value); - return _MinimumValue = value; - } - - set - { - double oldValue = _MinimumValue; - SetMinimumValue(oldValue, ref value); - if (oldValue != value) - { - _MinimumValue = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MetadataProviderId_Source_Id")] - public virtual MetadataProviderId Source { get; set; } - } -} - diff --git a/Jellyfin.Data/Entities/Release.cs b/Jellyfin.Data/Entities/Release.cs deleted file mode 100644 index 3e2cf22db..000000000 --- a/Jellyfin.Data/Entities/Release.cs +++ /dev/null @@ -1,219 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Release - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Release() - { - MediaFiles = new HashSet<MediaFile>(); - Chapters = new HashSet<Chapter>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Release CreateReleaseUnsafe() - { - return new Release(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="name"></param> - /// <param name="_movie0"></param> - /// <param name="_episode1"></param> - /// <param name="_track2"></param> - /// <param name="_customitem3"></param> - /// <param name="_book4"></param> - /// <param name="_photo5"></param> - public Release(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } - - this.Name = name; - - if (_movie0 == null) - { - throw new ArgumentNullException(nameof(_movie0)); - } - - _movie0.Releases.Add(this); - - if (_episode1 == null) - { - throw new ArgumentNullException(nameof(_episode1)); - } - - _episode1.Releases.Add(this); - - if (_track2 == null) - { - throw new ArgumentNullException(nameof(_track2)); - } - - _track2.Releases.Add(this); - - if (_customitem3 == null) - { - throw new ArgumentNullException(nameof(_customitem3)); - } - - _customitem3.Releases.Add(this); - - if (_book4 == null) - { - throw new ArgumentNullException(nameof(_book4)); - } - - _book4.Releases.Add(this); - - if (_photo5 == null) - { - throw new ArgumentNullException(nameof(_photo5)); - } - - _photo5.Releases.Add(this); - - this.MediaFiles = new HashSet<MediaFile>(); - this.Chapters = new HashSet<Chapter>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="name"></param> - /// <param name="_movie0"></param> - /// <param name="_episode1"></param> - /// <param name="_track2"></param> - /// <param name="_customitem3"></param> - /// <param name="_book4"></param> - /// <param name="_photo5"></param> - public static Release Create(string name, Movie _movie0, Episode _episode1, Track _track2, CustomItem _customitem3, Book _book4, Photo _photo5) - { - return new Release(name, _movie0, _episode1, _track2, _customitem3, _book4, _photo5); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Id. - /// </summary> - internal int _Id; - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before setting. - /// </summary> - partial void SetId(int oldValue, ref int newValue); - /// <summary> - /// When provided in a partial class, allows value of Id to be changed before returning. - /// </summary> - partial void GetId(ref int result); - - /// <summary> - /// Identity, Indexed, Required. - /// </summary> - [Key] - [Required] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id - { - get - { - int value = _Id; - GetId(ref value); - return _Id = value; - } - - protected set - { - int oldValue = _Id; - SetId(oldValue, ref value); - if (oldValue != value) - { - _Id = value; - } - } - } - - /// <summary> - /// Backing field for Name. - /// </summary> - protected string _Name; - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before setting. - /// </summary> - partial void SetName(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Name to be changed before returning. - /// </summary> - partial void GetName(ref string result); - - /// <summary> - /// Required, Max length = 1024 - /// </summary> - [Required] - [MaxLength(1024)] - [StringLength(1024)] - public string Name - { - get - { - string value = _Name; - GetName(ref value); - return _Name = value; - } - - set - { - string oldValue = _Name; - SetName(oldValue, ref value); - if (oldValue != value) - { - _Name = value; - } - } - } - - /// <summary> - /// Required, ConcurrenyToken. - /// </summary> - [ConcurrencyCheck] - [Required] - public uint RowVersion { get; set; } - - public void OnSavingChanges() - { - RowVersion++; - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("MediaFile_MediaFiles_Id")] - public virtual ICollection<MediaFile> MediaFiles { get; protected set; } - - [ForeignKey("Chapter_Chapters_Id")] - public virtual ICollection<Chapter> Chapters { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Season.cs b/Jellyfin.Data/Entities/Season.cs deleted file mode 100644 index e5e7d03ab..000000000 --- a/Jellyfin.Data/Entities/Season.cs +++ /dev/null @@ -1,119 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Season : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Season() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - SeasonMetadata = new HashSet<SeasonMetadata>(); - Episodes = new HashSet<Episode>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Season CreateSeasonUnsafe() - { - return new Season(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_series0"></param> - public Season(Guid urlid, DateTime dateadded, Series _series0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_series0 == null) - { - throw new ArgumentNullException(nameof(_series0)); - } - - _series0.Seasons.Add(this); - - this.SeasonMetadata = new HashSet<SeasonMetadata>(); - this.Episodes = new HashSet<Episode>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_series0"></param> - public static Season Create(Guid urlid, DateTime dateadded, Series _series0) - { - return new Season(urlid, dateadded, _series0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for SeasonNumber. - /// </summary> - protected int? _SeasonNumber; - /// <summary> - /// When provided in a partial class, allows value of SeasonNumber to be changed before setting. - /// </summary> - partial void SetSeasonNumber(int? oldValue, ref int? newValue); - /// <summary> - /// When provided in a partial class, allows value of SeasonNumber to be changed before returning. - /// </summary> - partial void GetSeasonNumber(ref int? result); - - public int? SeasonNumber - { - get - { - int? value = _SeasonNumber; - GetSeasonNumber(ref value); - return _SeasonNumber = value; - } - - set - { - int? oldValue = _SeasonNumber; - SetSeasonNumber(oldValue, ref value); - if (oldValue != value) - { - _SeasonNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("SeasonMetadata_SeasonMetadata_Id")] - public virtual ICollection<SeasonMetadata> SeasonMetadata { get; protected set; } - - [ForeignKey("Episode_Episodes_Id")] - public virtual ICollection<Episode> Episodes { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/SeasonMetadata.cs b/Jellyfin.Data/Entities/SeasonMetadata.cs deleted file mode 100644 index cce8cb125..000000000 --- a/Jellyfin.Data/Entities/SeasonMetadata.cs +++ /dev/null @@ -1,123 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class SeasonMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected SeasonMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static SeasonMetadata CreateSeasonMetadataUnsafe() - { - return new SeasonMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_season0"></param> - public SeasonMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_season0 == null) - { - throw new ArgumentNullException(nameof(_season0)); - } - - _season0.SeasonMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_season0"></param> - public static SeasonMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Season _season0) - { - return new SeasonMetadata(title, language, dateadded, datemodified, _season0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Outline. - /// </summary> - protected string _Outline; - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// </summary> - partial void SetOutline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// </summary> - partial void GetOutline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/Series.cs b/Jellyfin.Data/Entities/Series.cs deleted file mode 100644 index 33c07ca61..000000000 --- a/Jellyfin.Data/Entities/Series.cs +++ /dev/null @@ -1,165 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Series : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Series() - { - SeriesMetadata = new HashSet<SeriesMetadata>(); - Seasons = new HashSet<Season>(); - - Init(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public Series(Guid urlid, DateTime dateadded) - { - this.UrlId = urlid; - - this.SeriesMetadata = new HashSet<SeriesMetadata>(); - this.Seasons = new HashSet<Season>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - public static Series Create(Guid urlid, DateTime dateadded) - { - return new Series(urlid, dateadded); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for AirsDayOfWeek. - /// </summary> - protected DayOfWeek? _AirsDayOfWeek; - /// <summary> - /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting. - /// </summary> - partial void SetAirsDayOfWeek(DayOfWeek? oldValue, ref DayOfWeek? newValue); - /// <summary> - /// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning. - /// </summary> - partial void GetAirsDayOfWeek(ref DayOfWeek? result); - - public DayOfWeek? AirsDayOfWeek - { - get - { - DayOfWeek? value = _AirsDayOfWeek; - GetAirsDayOfWeek(ref value); - return _AirsDayOfWeek = value; - } - - set - { - DayOfWeek? oldValue = _AirsDayOfWeek; - SetAirsDayOfWeek(oldValue, ref value); - if (oldValue != value) - { - _AirsDayOfWeek = value; - } - } - } - - /// <summary> - /// Backing field for AirsTime. - /// </summary> - protected DateTimeOffset? _AirsTime; - /// <summary> - /// When provided in a partial class, allows value of AirsTime to be changed before setting. - /// </summary> - partial void SetAirsTime(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// <summary> - /// When provided in a partial class, allows value of AirsTime to be changed before returning. - /// </summary> - partial void GetAirsTime(ref DateTimeOffset? result); - - /// <summary> - /// The time the show airs, ignore the date portion. - /// </summary> - public DateTimeOffset? AirsTime - { - get - { - DateTimeOffset? value = _AirsTime; - GetAirsTime(ref value); - return _AirsTime = value; - } - - set - { - DateTimeOffset? oldValue = _AirsTime; - SetAirsTime(oldValue, ref value); - if (oldValue != value) - { - _AirsTime = value; - } - } - } - - /// <summary> - /// Backing field for FirstAired. - /// </summary> - protected DateTimeOffset? _FirstAired; - /// <summary> - /// When provided in a partial class, allows value of FirstAired to be changed before setting. - /// </summary> - partial void SetFirstAired(DateTimeOffset? oldValue, ref DateTimeOffset? newValue); - /// <summary> - /// When provided in a partial class, allows value of FirstAired to be changed before returning. - /// </summary> - partial void GetFirstAired(ref DateTimeOffset? result); - - public DateTimeOffset? FirstAired - { - get - { - DateTimeOffset? value = _FirstAired; - GetFirstAired(ref value); - return _FirstAired = value; - } - - set - { - DateTimeOffset? oldValue = _FirstAired; - SetFirstAired(oldValue, ref value); - if (oldValue != value) - { - _FirstAired = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("SeriesMetadata_SeriesMetadata_Id")] - public virtual ICollection<SeriesMetadata> SeriesMetadata { get; protected set; } - - [ForeignKey("Season_Seasons_Id")] - public virtual ICollection<Season> Seasons { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/SeriesMetadata.cs b/Jellyfin.Data/Entities/SeriesMetadata.cs deleted file mode 100644 index 22be2a59b..000000000 --- a/Jellyfin.Data/Entities/SeriesMetadata.cs +++ /dev/null @@ -1,244 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class SeriesMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected SeriesMetadata() - { - Networks = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static SeriesMetadata CreateSeriesMetadataUnsafe() - { - return new SeriesMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_series0"></param> - public SeriesMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_series0 == null) - { - throw new ArgumentNullException(nameof(_series0)); - } - - _series0.SeriesMetadata.Add(this); - - this.Networks = new HashSet<Company>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_series0"></param> - public static SeriesMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Series _series0) - { - return new SeriesMetadata(title, language, dateadded, datemodified, _series0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for Outline. - /// </summary> - protected string _Outline; - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before setting. - /// </summary> - partial void SetOutline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Outline to be changed before returning. - /// </summary> - partial void GetOutline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Outline - { - get - { - string value = _Outline; - GetOutline(ref value); - return _Outline = value; - } - - set - { - string oldValue = _Outline; - SetOutline(oldValue, ref value); - if (oldValue != value) - { - _Outline = value; - } - } - } - - /// <summary> - /// Backing field for Plot. - /// </summary> - protected string _Plot; - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before setting. - /// </summary> - partial void SetPlot(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Plot to be changed before returning. - /// </summary> - partial void GetPlot(ref string result); - - /// <summary> - /// Max length = 65535 - /// </summary> - [MaxLength(65535)] - [StringLength(65535)] - public string Plot - { - get - { - string value = _Plot; - GetPlot(ref value); - return _Plot = value; - } - - set - { - string oldValue = _Plot; - SetPlot(oldValue, ref value); - if (oldValue != value) - { - _Plot = value; - } - } - } - - /// <summary> - /// Backing field for Tagline. - /// </summary> - protected string _Tagline; - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before setting. - /// </summary> - partial void SetTagline(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Tagline to be changed before returning. - /// </summary> - partial void GetTagline(ref string result); - - /// <summary> - /// Max length = 1024 - /// </summary> - [MaxLength(1024)] - [StringLength(1024)] - public string Tagline - { - get - { - string value = _Tagline; - GetTagline(ref value); - return _Tagline = value; - } - - set - { - string oldValue = _Tagline; - SetTagline(oldValue, ref value); - if (oldValue != value) - { - _Tagline = value; - } - } - } - - /// <summary> - /// Backing field for Country. - /// </summary> - protected string _Country; - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before setting. - /// </summary> - partial void SetCountry(string oldValue, ref string newValue); - /// <summary> - /// When provided in a partial class, allows value of Country to be changed before returning. - /// </summary> - partial void GetCountry(ref string result); - - /// <summary> - /// Max length = 2 - /// </summary> - [MaxLength(2)] - [StringLength(2)] - public string Country - { - get - { - string value = _Country; - GetCountry(ref value); - return _Country = value; - } - - set - { - string oldValue = _Country; - SetCountry(oldValue, ref value); - if (oldValue != value) - { - _Country = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - [ForeignKey("Company_Networks_Id")] - public virtual ICollection<Company> Networks { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/Track.cs b/Jellyfin.Data/Entities/Track.cs deleted file mode 100644 index d52dd725a..000000000 --- a/Jellyfin.Data/Entities/Track.cs +++ /dev/null @@ -1,120 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; - -namespace Jellyfin.Data.Entities -{ - public partial class Track : LibraryItem - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected Track() - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - Releases = new HashSet<Release>(); - TrackMetadata = new HashSet<TrackMetadata>(); - - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static Track CreateTrackUnsafe() - { - return new Track(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_musicalbum0"></param> - public Track(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) - { - // NOTE: This class has one-to-one associations with LibraryRoot, LibraryItem and CollectionItem. - // One-to-one associations are not validated in constructors since this causes a scenario where each one must be constructed before the other. - - this.UrlId = urlid; - - if (_musicalbum0 == null) - { - throw new ArgumentNullException(nameof(_musicalbum0)); - } - - _musicalbum0.Tracks.Add(this); - - this.Releases = new HashSet<Release>(); - this.TrackMetadata = new HashSet<TrackMetadata>(); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="urlid">This is whats gets displayed in the Urls and API requests. This could also be a string.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="_musicalbum0"></param> - public static Track Create(Guid urlid, DateTime dateadded, MusicAlbum _musicalbum0) - { - return new Track(urlid, dateadded, _musicalbum0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /// <summary> - /// Backing field for TrackNumber. - /// </summary> - protected int? _TrackNumber; - /// <summary> - /// When provided in a partial class, allows value of TrackNumber to be changed before setting. - /// </summary> - partial void SetTrackNumber(int? oldValue, ref int? newValue); - /// <summary> - /// When provided in a partial class, allows value of TrackNumber to be changed before returning. - /// </summary> - partial void GetTrackNumber(ref int? result); - - public int? TrackNumber - { - get - { - int? value = _TrackNumber; - GetTrackNumber(ref value); - return _TrackNumber = value; - } - - set - { - int? oldValue = _TrackNumber; - SetTrackNumber(oldValue, ref value); - if (oldValue != value) - { - _TrackNumber = value; - } - } - } - - /************************************************************************* - * Navigation properties - *************************************************************************/ - - [ForeignKey("Release_Releases_Id")] - public virtual ICollection<Release> Releases { get; protected set; } - - [ForeignKey("TrackMetadata_TrackMetadata_Id")] - public virtual ICollection<TrackMetadata> TrackMetadata { get; protected set; } - } -} - diff --git a/Jellyfin.Data/Entities/TrackMetadata.cs b/Jellyfin.Data/Entities/TrackMetadata.cs deleted file mode 100644 index 710908eb8..000000000 --- a/Jellyfin.Data/Entities/TrackMetadata.cs +++ /dev/null @@ -1,83 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace Jellyfin.Data.Entities -{ - public partial class TrackMetadata : Metadata - { - partial void Init(); - - /// <summary> - /// Default constructor. Protected due to required properties, but present because EF needs it. - /// </summary> - protected TrackMetadata() - { - Init(); - } - - /// <summary> - /// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving. - /// </summary> - public static TrackMetadata CreateTrackMetadataUnsafe() - { - return new TrackMetadata(); - } - - /// <summary> - /// Public constructor with required data. - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_track0"></param> - public TrackMetadata(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) - { - if (string.IsNullOrEmpty(title)) - { - throw new ArgumentNullException(nameof(title)); - } - - this.Title = title; - - if (string.IsNullOrEmpty(language)) - { - throw new ArgumentNullException(nameof(language)); - } - - this.Language = language; - - if (_track0 == null) - { - throw new ArgumentNullException(nameof(_track0)); - } - - _track0.TrackMetadata.Add(this); - - Init(); - } - - /// <summary> - /// Static create function (for use in LINQ queries, etc.) - /// </summary> - /// <param name="title">The title or name of the object.</param> - /// <param name="language">ISO-639-3 3-character language codes.</param> - /// <param name="dateadded">The date the object was added.</param> - /// <param name="datemodified">The date the object was last modified.</param> - /// <param name="_track0"></param> - public static TrackMetadata Create(string title, string language, DateTime dateadded, DateTime datemodified, Track _track0) - { - return new TrackMetadata(title, language, dateadded, datemodified, _track0); - } - - /************************************************************************* - * Properties - *************************************************************************/ - - /************************************************************************* - * Navigation properties - *************************************************************************/ - } -} - diff --git a/Jellyfin.Data/Entities/User.cs b/Jellyfin.Data/Entities/User.cs index 8c720d85b..7ea1f4498 100644 --- a/Jellyfin.Data/Entities/User.cs +++ b/Jellyfin.Data/Entities/User.cs @@ -8,13 +8,14 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Data.Interfaces; namespace Jellyfin.Data.Entities { /// <summary> /// An entity representing a user. /// </summary> - public partial class User : IHasPermissions, ISavingChanges + public partial class User : IHasPermissions, IHasConcurrencyToken { /// <summary> /// The values being delimited here are Guids, so commas work as they do not appear in Guids. diff --git a/Jellyfin.Data/ISavingChanges.cs b/Jellyfin.Data/ISavingChanges.cs deleted file mode 100644 index f392dae6a..000000000 --- a/Jellyfin.Data/ISavingChanges.cs +++ /dev/null @@ -1,9 +0,0 @@ -#pragma warning disable CS1591 - -namespace Jellyfin.Data -{ - public interface ISavingChanges - { - void OnSavingChanges(); - } -} diff --git a/Jellyfin.Data/Interfaces/IHasArtwork.cs b/Jellyfin.Data/Interfaces/IHasArtwork.cs new file mode 100644 index 000000000..a4d9c54af --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasArtwork.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// <summary> + /// An interface abstracting an entity that has artwork. + /// </summary> + public interface IHasArtwork + { + /// <summary> + /// Gets a collection containing this entity's artwork. + /// </summary> + ICollection<Artwork> Artwork { get; } + } +} diff --git a/Jellyfin.Data/Interfaces/IHasCompanies.cs b/Jellyfin.Data/Interfaces/IHasCompanies.cs new file mode 100644 index 000000000..8f19ce04f --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasCompanies.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// <summary> + /// An abstraction representing an entity that has companies. + /// </summary> + public interface IHasCompanies + { + /// <summary> + /// Gets a collection containing this entity's companies. + /// </summary> + ICollection<Company> Companies { get; } + } +} diff --git a/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs b/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs new file mode 100644 index 000000000..2c4091493 --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasConcurrencyToken.cs @@ -0,0 +1,18 @@ +namespace Jellyfin.Data.Interfaces +{ + /// <summary> + /// An interface abstracting an entity that has a concurrency token. + /// </summary> + public interface IHasConcurrencyToken + { + /// <summary> + /// Gets the version of this row. Acts as a concurrency token. + /// </summary> + uint RowVersion { get; } + + /// <summary> + /// Called when saving changes to this entity. + /// </summary> + void OnSavingChanges(); + } +} diff --git a/Jellyfin.Data/IHasPermissions.cs b/Jellyfin.Data/Interfaces/IHasPermissions.cs index 3be72259a..3be72259a 100644 --- a/Jellyfin.Data/IHasPermissions.cs +++ b/Jellyfin.Data/Interfaces/IHasPermissions.cs diff --git a/Jellyfin.Data/Interfaces/IHasReleases.cs b/Jellyfin.Data/Interfaces/IHasReleases.cs new file mode 100644 index 000000000..3b615893e --- /dev/null +++ b/Jellyfin.Data/Interfaces/IHasReleases.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Jellyfin.Data.Entities.Libraries; + +namespace Jellyfin.Data.Interfaces +{ + /// <summary> + /// An abstraction representing an entity that has releases. + /// </summary> + public interface IHasReleases + { + /// <summary> + /// Gets a collection containing this entity's releases. + /// </summary> + ICollection<Release> Releases { get; } + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index e8065419d..203eeaf3b 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -5,6 +5,15 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> <PropertyGroup> @@ -19,6 +28,10 @@ <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> + </ItemGroup> + <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8" PrivateAssets="All" /> @@ -28,8 +41,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.6" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.6" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.7" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.7" /> </ItemGroup> </Project> diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index 21748ca19..30ed3e6af 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -24,11 +24,11 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.6"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="3.1.7"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server.Implementations/JellyfinDb.cs b/Jellyfin.Server.Implementations/JellyfinDb.cs index 7d864ebc6..08e4db388 100644 --- a/Jellyfin.Server.Implementations/JellyfinDb.cs +++ b/Jellyfin.Server.Implementations/JellyfinDb.cs @@ -2,8 +2,8 @@ using System; using System.Linq; -using Jellyfin.Data; using Jellyfin.Data.Entities; +using Jellyfin.Data.Interfaces; using Microsoft.EntityFrameworkCore; namespace Jellyfin.Server.Implementations @@ -130,7 +130,7 @@ namespace Jellyfin.Server.Implementations foreach (var saveEntity in ChangeTracker.Entries() .Where(e => e.State == EntityState.Modified) .Select(entry => entry.Entity) - .OfType<ISavingChanges>()) + .OfType<IHasConcurrencyToken>()) { saveEntity.OnSavingChanges(); } diff --git a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs index 745567703..2b002c6f3 100644 --- a/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiApplicationBuilderExtensions.cs @@ -1,3 +1,4 @@ +using Jellyfin.Server.Middleware; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -38,13 +39,65 @@ namespace Jellyfin.Server.Extensions c.DocumentTitle = "Jellyfin API"; c.SwaggerEndpoint($"/{baseUrl}api-docs/openapi.json", "Jellyfin API"); c.RoutePrefix = $"{baseUrl}api-docs/swagger"; + c.InjectStylesheet($"/{baseUrl}api-docs/swagger/custom.css"); }) .UseReDoc(c => { c.DocumentTitle = "Jellyfin API"; c.SpecUrl($"/{baseUrl}api-docs/openapi.json"); c.RoutePrefix = $"{baseUrl}api-docs/redoc"; + c.InjectStylesheet($"/{baseUrl}api-docs/redoc/custom.css"); }); } + + /// <summary> + /// Adds IP based access validation to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseIpBasedAccessValidation(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<IpBasedAccessValidationMiddleware>(); + } + + /// <summary> + /// Adds LAN based access filtering to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseLanFiltering(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<LanFilteringMiddleware>(); + } + + /// <summary> + /// Adds base url redirection to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseBaseUrlRedirection(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<BaseUrlRedirectionMiddleware>(); + } + + /// <summary> + /// Adds a custom message during server startup to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseServerStartupMessage(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<ServerStartupMessageMiddleware>(); + } + + /// <summary> + /// Adds a WebSocket request handler to the application pipeline. + /// </summary> + /// <param name="appBuilder">The application builder.</param> + /// <returns>The updated application builder.</returns> + public static IApplicationBuilder UseWebSocketHandler(this IApplicationBuilder appBuilder) + { + return appBuilder.UseMiddleware<WebSocketHandlerMiddleware>(); + } } } diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs index 0fd599cfc..0160a05f9 100644 --- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs +++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Controllers; using Jellyfin.Server.Formatters; using Jellyfin.Server.Models; +using MediaBrowser.Common; using MediaBrowser.Common.Json; using MediaBrowser.Model.Entities; using Microsoft.AspNetCore.Authentication; @@ -135,10 +136,11 @@ namespace Jellyfin.Server.Extensions /// </summary> /// <param name="serviceCollection">The service collection.</param> /// <param name="baseUrl">The base url for the API.</param> + /// <param name="pluginAssemblies">An IEnumberable containing all plugin assemblies with API controllers.</param> /// <returns>The MVC builder.</returns> - public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl) + public static IMvcBuilder AddJellyfinApi(this IServiceCollection serviceCollection, string baseUrl, IEnumerable<Assembly> pluginAssemblies) { - return serviceCollection + IMvcBuilder mvcBuilder = serviceCollection .AddCors(options => { options.AddPolicy(ServerCorsPolicy.DefaultPolicyName, ServerCorsPolicy.DefaultPolicy); @@ -179,8 +181,14 @@ namespace Jellyfin.Server.Extensions // From JsonDefaults.PascalCase options.JsonSerializerOptions.PropertyNamingPolicy = jsonOptions.PropertyNamingPolicy; - }) - .AddControllersAsServices(); + }); + + foreach (Assembly pluginAssembly in pluginAssemblies) + { + mvcBuilder.AddApplicationPart(pluginAssembly); + } + + return mvcBuilder.AddControllersAsServices(); } /// <summary> @@ -197,7 +205,7 @@ namespace Jellyfin.Server.Extensions { Type = SecuritySchemeType.ApiKey, In = ParameterLocation.Header, - Name = "X-Emby-Token", + Name = "X-Emby-Authorization", Description = "API key header parameter" }); diff --git a/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs b/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs new file mode 100644 index 000000000..aea684479 --- /dev/null +++ b/Jellyfin.Server/HealthChecks/JellyfinDbHealthCheck.cs @@ -0,0 +1,36 @@ +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Server.Implementations; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Jellyfin.Server.HealthChecks +{ + /// <summary> + /// Checks connectivity to the database. + /// </summary> + public class JellyfinDbHealthCheck : IHealthCheck + { + private readonly JellyfinDbProvider _dbProvider; + + /// <summary> + /// Initializes a new instance of the <see cref="JellyfinDbHealthCheck"/> class. + /// </summary> + /// <param name="dbProvider">The jellyfin db provider.</param> + public JellyfinDbHealthCheck(JellyfinDbProvider dbProvider) + { + _dbProvider = dbProvider; + } + + /// <inheritdoc /> + public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + await using var jellyfinDb = _dbProvider.CreateContext(); + if (await jellyfinDb.Database.CanConnectAsync(cancellationToken).ConfigureAwait(false)) + { + return HealthCheckResult.Healthy("Database connection successful."); + } + + return HealthCheckResult.Unhealthy("Unable to connect to the database."); + } + } +} diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 7541707d9..24ba8369a 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -41,8 +41,9 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="3.1.7" /> <PackageReference Include="prometheus-net" Version="3.6.0" /> <PackageReference Include="prometheus-net.AspNetCore" Version="3.6.0" /> <PackageReference Include="Serilog.AspNetCore" Version="3.4.0" /> @@ -63,4 +64,13 @@ <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> </ItemGroup> + <ItemGroup> + <None Update="wwwroot\api-docs\swagger\custom.css"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + <None Update="wwwroot\api-docs\redoc\custom.css"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + </Project> diff --git a/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs new file mode 100644 index 000000000..9316737bd --- /dev/null +++ b/Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Redirect requests without baseurl prefix to the baseurl prefixed URL. + /// </summary> + public class BaseUrlRedirectionMiddleware + { + private readonly RequestDelegate _next; + private readonly ILogger<BaseUrlRedirectionMiddleware> _logger; + private readonly IConfiguration _configuration; + + /// <summary> + /// Initializes a new instance of the <see cref="BaseUrlRedirectionMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + /// <param name="logger">The logger.</param> + /// <param name="configuration">The application configuration.</param> + public BaseUrlRedirectionMiddleware( + RequestDelegate next, + ILogger<BaseUrlRedirectionMiddleware> logger, + IConfiguration configuration) + { + _next = next; + _logger = logger; + _configuration = configuration; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext, IServerConfigurationManager serverConfigurationManager) + { + var localPath = httpContext.Request.Path.ToString(); + var baseUrlPrefix = serverConfigurationManager.Configuration.BaseUrl; + + if (string.Equals(localPath, baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, baseUrlPrefix, StringComparison.OrdinalIgnoreCase) + || string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(localPath) + || !localPath.StartsWith(baseUrlPrefix, StringComparison.OrdinalIgnoreCase)) + { + // Always redirect back to the default path if the base prefix is invalid or missing + _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath); + httpContext.Response.Redirect(baseUrlPrefix + "/" + _configuration[ConfigurationExtensions.DefaultRedirectKey]); + return; + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs new file mode 100644 index 000000000..59b5fb1ed --- /dev/null +++ b/Jellyfin.Server/Middleware/IpBasedAccessValidationMiddleware.cs @@ -0,0 +1,76 @@ +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Validates the IP of requests coming from local networks wrt. remote access. + /// </summary> + public class IpBasedAccessValidationMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="IpBasedAccessValidationMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public IpBasedAccessValidationMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <param name="networkManager">The network manager.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + if (httpContext.Request.IsLocal()) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + var remoteIp = httpContext.Request.RemoteIp(); + + if (serverConfigurationManager.Configuration.EnableRemoteAccess) + { + var addressFilter = serverConfigurationManager.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray(); + + if (addressFilter.Length > 0 && !networkManager.IsInLocalNetwork(remoteIp)) + { + if (serverConfigurationManager.Configuration.IsRemoteIPFilterBlacklist) + { + if (networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + else + { + if (!networkManager.IsAddressInSubnets(remoteIp, addressFilter)) + { + return; + } + } + } + } + else + { + if (!networkManager.IsInLocalNetwork(remoteIp)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs new file mode 100644 index 000000000..9d795145a --- /dev/null +++ b/Jellyfin.Server/Middleware/LanFilteringMiddleware.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller.Configuration; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Validates the LAN host IP based on application configuration. + /// </summary> + public class LanFilteringMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="LanFilteringMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public LanFilteringMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <param name="networkManager">The network manager.</param> + /// <param name="serverConfigurationManager">The server configuration manager.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext, INetworkManager networkManager, IServerConfigurationManager serverConfigurationManager) + { + var currentHost = httpContext.Request.Host.ToString(); + var hosts = serverConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .ToList(); + + if (hosts.Count == 0) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + currentHost ??= string.Empty; + + if (networkManager.IsInPrivateAddressSpace(currentHost)) + { + hosts.Add("localhost"); + hosts.Add("127.0.0.1"); + + if (hosts.All(i => currentHost.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1)) + { + return; + } + } + + await _next(httpContext).ConfigureAwait(false); + } + + private static string NormalizeConfiguredLocalAddress(string address) + { + var add = address.AsSpan().Trim('/'); + int index = add.IndexOf('/'); + if (index != -1) + { + add = add.Slice(index + 1); + } + + return add.TrimStart('/').ToString(); + } + } +} diff --git a/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs new file mode 100644 index 000000000..ea81c03a2 --- /dev/null +++ b/Jellyfin.Server/Middleware/ServerStartupMessageMiddleware.cs @@ -0,0 +1,49 @@ +using System.Net.Mime; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Model.Globalization; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Shows a custom message during server startup. + /// </summary> + public class ServerStartupMessageMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="ServerStartupMessageMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public ServerStartupMessageMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <param name="serverApplicationHost">The server application host.</param> + /// <param name="localizationManager">The localization manager.</param> + /// <returns>The async task.</returns> + public async Task Invoke( + HttpContext httpContext, + IServerApplicationHost serverApplicationHost, + ILocalizationManager localizationManager) + { + if (serverApplicationHost.CoreStartupHasCompleted) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + var message = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + httpContext.Response.ContentType = MediaTypeNames.Text.Html; + await httpContext.Response.WriteAsync(message, httpContext.RequestAborted).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs new file mode 100644 index 000000000..b7a5d2b34 --- /dev/null +++ b/Jellyfin.Server/Middleware/WebSocketHandlerMiddleware.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using Microsoft.AspNetCore.Http; + +namespace Jellyfin.Server.Middleware +{ + /// <summary> + /// Handles WebSocket requests. + /// </summary> + public class WebSocketHandlerMiddleware + { + private readonly RequestDelegate _next; + + /// <summary> + /// Initializes a new instance of the <see cref="WebSocketHandlerMiddleware"/> class. + /// </summary> + /// <param name="next">The next delegate in the pipeline.</param> + public WebSocketHandlerMiddleware(RequestDelegate next) + { + _next = next; + } + + /// <summary> + /// Executes the middleware action. + /// </summary> + /// <param name="httpContext">The current HTTP context.</param> + /// <param name="webSocketManager">The WebSocket connection manager.</param> + /// <returns>The async task.</returns> + public async Task Invoke(HttpContext httpContext, IWebSocketManager webSocketManager) + { + if (!httpContext.WebSockets.IsWebSocketRequest) + { + await _next(httpContext).ConfigureAwait(false); + return; + } + + await webSocketManager.WebSocketRequestHandler(httpContext).ConfigureAwait(false); + } + } +} diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 14cc5f4c2..b9a90f9db 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -11,7 +11,6 @@ using System.Threading; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Emby.Server.Implementations.HttpServer; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Networking; using Jellyfin.Api.Controllers; @@ -28,6 +27,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; using SQLitePCL; +using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server @@ -594,7 +594,7 @@ namespace Jellyfin.Server var inMemoryDefaultConfig = ConfigurationOptions.DefaultConfiguration; if (startupConfig != null && !startupConfig.HostWebClient()) { - inMemoryDefaultConfig[HttpListenerHost.DefaultRedirectKey] = "api-docs/swagger"; + inMemoryDefaultConfig[ConfigurationExtensions.DefaultRedirectKey] = "api-docs/swagger"; } return config diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index d0dd183c6..1a34de269 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -1,9 +1,12 @@ using System; using System.ComponentModel; +using System.Net.Http.Headers; using Jellyfin.Api.TypeConverters; using Jellyfin.Server.Extensions; +using Jellyfin.Server.HealthChecks; using Jellyfin.Server.Middleware; using Jellyfin.Server.Models; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using Microsoft.AspNetCore.Builder; @@ -20,14 +23,19 @@ namespace Jellyfin.Server public class Startup { private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IServerApplicationHost _serverApplicationHost; /// <summary> /// Initializes a new instance of the <see cref="Startup" /> class. /// </summary> /// <param name="serverConfigurationManager">The server configuration manager.</param> - public Startup(IServerConfigurationManager serverConfigurationManager) + /// <param name="serverApplicationHost">The server application host.</param> + public Startup( + IServerConfigurationManager serverConfigurationManager, + IServerApplicationHost serverApplicationHost) { _serverConfigurationManager = serverConfigurationManager; + _serverApplicationHost = serverApplicationHost; } /// <summary> @@ -38,7 +46,13 @@ namespace Jellyfin.Server { services.AddResponseCompression(); services.AddHttpContextAccessor(); - services.AddJellyfinApi(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/')); + services.AddHttpsRedirection(options => + { + options.HttpsPort = _serverApplicationHost.HttpsPort; + }); + services.AddJellyfinApi( + _serverConfigurationManager.Configuration.BaseUrl.TrimStart('/'), + _serverApplicationHost.GetApiPluginAssemblies()); services.AddJellyfinApiSwagger(); @@ -46,7 +60,26 @@ namespace Jellyfin.Server services.AddCustomAuthentication(); services.AddJellyfinApiAuthorization(); - services.AddHttpClient(); + + var productHeader = new ProductInfoHeaderValue( + _serverApplicationHost.Name.Replace(' ', '-'), + _serverApplicationHost.ApplicationVersionString); + services + .AddHttpClient(NamedClient.Default, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + }) + .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + + services.AddHttpClient(NamedClient.MusicBrainz, c => + { + c.DefaultRequestHeaders.UserAgent.Add(productHeader); + c.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue($"({_serverApplicationHost.ApplicationUserAgentAddress})")); + }) + .ConfigurePrimaryHttpMessageHandler(x => new DefaultHttpClientHandler()); + + services.AddHealthChecks() + .AddCheck<JellyfinDbHealthCheck>("JellyfinDb"); } /// <summary> @@ -54,11 +87,9 @@ namespace Jellyfin.Server /// </summary> /// <param name="app">The application builder.</param> /// <param name="env">The webhost environment.</param> - /// <param name="serverApplicationHost">The server application host.</param> public void Configure( IApplicationBuilder app, - IWebHostEnvironment env, - IServerApplicationHost serverApplicationHost) + IWebHostEnvironment env) { if (env.IsDevelopment()) { @@ -73,12 +104,18 @@ namespace Jellyfin.Server app.UseResponseCompression(); - // TODO app.UseMiddleware<WebSocketMiddleware>(); + app.UseCors(ServerCorsPolicy.DefaultPolicyName); + + if (_serverConfigurationManager.Configuration.RequireHttps + && _serverApplicationHost.ListenWithHttps) + { + app.UseHttpsRedirection(); + } + app.UseStaticFiles(); app.UseAuthentication(); app.UseJellyfinApiSwagger(_serverConfigurationManager); app.UseRouting(); - app.UseCors(ServerCorsPolicy.DefaultPolicyName); app.UseAuthorization(); if (_serverConfigurationManager.Configuration.EnableMetrics) { @@ -86,6 +123,12 @@ namespace Jellyfin.Server app.UseHttpMetrics(); } + app.UseLanFiltering(); + app.UseIpBasedAccessValidation(); + app.UseBaseUrlRedirection(); + app.UseWebSocketHandler(); + app.UseServerStartupMessage(); + app.UseEndpoints(endpoints => { endpoints.MapControllers(); @@ -93,9 +136,9 @@ namespace Jellyfin.Server { endpoints.MapMetrics(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/metrics"); } - }); - app.Use(serverApplicationHost.ExecuteHttpHandlerAsync); + endpoints.MapHealthChecks(_serverConfigurationManager.Configuration.BaseUrl.TrimStart('/') + "/health"); + }); // Add type descriptor for legacy datetime parsing. TypeDescriptor.AddAttributes(typeof(DateTime?), new TypeConverterAttribute(typeof(DateTimeTypeConverter))); diff --git a/Jellyfin.Server/wwwroot/api-docs/redoc/custom.css b/Jellyfin.Server/wwwroot/api-docs/redoc/custom.css new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/redoc/custom.css diff --git a/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/Jellyfin.Server/wwwroot/api-docs/swagger/custom.css diff --git a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs index d746207c7..e0cf3f9ac 100644 --- a/MediaBrowser.Common/Extensions/HttpContextExtensions.cs +++ b/MediaBrowser.Common/Extensions/HttpContextExtensions.cs @@ -1,4 +1,5 @@ -using MediaBrowser.Model.Services; +using System.Net; +using MediaBrowser.Common.Net; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Common.Extensions @@ -8,26 +9,55 @@ namespace MediaBrowser.Common.Extensions /// </summary> public static class HttpContextExtensions { - private const string ServiceStackRequest = "ServiceStackRequest"; - /// <summary> - /// Set the ServiceStack request. + /// Checks the origin of the HTTP request. /// </summary> - /// <param name="httpContext">The HttpContext instance.</param> - /// <param name="request">The service stack request instance.</param> - public static void SetServiceStackRequest(this HttpContext httpContext, IRequest request) + /// <param name="request">The incoming HTTP request.</param> + /// <returns><c>true</c> if the request is coming from LAN, <c>false</c> otherwise.</returns> + public static bool IsLocal(this HttpRequest request) { - httpContext.Items[ServiceStackRequest] = request; + return (request.HttpContext.Connection.LocalIpAddress == null + && request.HttpContext.Connection.RemoteIpAddress == null) + || request.HttpContext.Connection.LocalIpAddress.Equals(request.HttpContext.Connection.RemoteIpAddress); } /// <summary> - /// Get the ServiceStack request. + /// Extracts the remote IP address of the caller of the HTTP request. /// </summary> - /// <param name="httpContext">The HttpContext instance.</param> - /// <returns>The service stack request instance.</returns> - public static IRequest GetServiceStackRequest(this HttpContext httpContext) + /// <param name="request">The HTTP request.</param> + /// <returns>The remote caller IP address.</returns> + public static string RemoteIp(this HttpRequest request) { - return (IRequest)httpContext.Items[ServiceStackRequest]; + var cachedRemoteIp = request.HttpContext.Items["RemoteIp"]?.ToString(); + if (!string.IsNullOrEmpty(cachedRemoteIp)) + { + return cachedRemoteIp; + } + + IPAddress ip; + + // "Real" remote ip might be in X-Forwarded-For of X-Real-Ip + // (if the server is behind a reverse proxy for example) + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XForwardedFor].ToString(), out ip)) + { + if (!IPAddress.TryParse(request.Headers[CustomHeaderNames.XRealIP].ToString(), out ip)) + { + ip = request.HttpContext.Connection.RemoteIpAddress; + + // Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests) + ip ??= IPAddress.Loopback; + } + } + + if (ip.IsIPv4MappedToIPv6) + { + ip = ip.MapToIPv4(); + } + + var normalizedIp = ip.ToString(); + + request.HttpContext.Items["RemoteIp"] = normalizedIp; + return normalizedIp; } } } diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index 7a76be722..849037ac4 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Threading.Tasks; using MediaBrowser.Common.Plugins; using Microsoft.Extensions.DependencyInjection; @@ -77,6 +78,12 @@ namespace MediaBrowser.Common IReadOnlyList<IPlugin> Plugins { get; } /// <summary> + /// Gets all plugin assemblies which implement a custom rest api. + /// </summary> + /// <returns>An <see cref="IEnumerable{Assembly}"/> containing the plugin assemblies.</returns> + IEnumerable<Assembly> GetApiPluginAssemblies(); + + /// <summary> /// Notifies the pending restart. /// </summary> void NotifyPendingRestart(); diff --git a/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs new file mode 100644 index 000000000..cffc41ba3 --- /dev/null +++ b/MediaBrowser.Common/Json/Converters/JsonNullableStructConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MediaBrowser.Common.Json.Converters +{ + /// <summary> + /// Converts a nullable struct or value to/from JSON. + /// Required - some clients send an empty string. + /// </summary> + /// <typeparam name="T">The struct type.</typeparam> + public class JsonNullableStructConverter<T> : JsonConverter<T?> + where T : struct + { + private readonly JsonConverter<T?> _baseJsonConverter; + + /// <summary> + /// Initializes a new instance of the <see cref="JsonNullableStructConverter{T}"/> class. + /// </summary> + /// <param name="baseJsonConverter">The base json converter.</param> + public JsonNullableStructConverter(JsonConverter<T?> baseJsonConverter) + { + _baseJsonConverter = baseJsonConverter; + } + + /// <inheritdoc /> + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + // Handle empty string. + if (reader.TokenType == JsonTokenType.String && ((reader.HasValueSequence && reader.ValueSequence.IsEmpty) || reader.ValueSpan.IsEmpty)) + { + return null; + } + + return _baseJsonConverter.Read(ref reader, typeToConvert, options); + } + + /// <inheritdoc /> + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) + { + _baseJsonConverter.Write(writer, value, options); + } + } +} diff --git a/MediaBrowser.Common/Json/JsonDefaults.cs b/MediaBrowser.Common/Json/JsonDefaults.cs index 9d30927db..5867cd4a0 100644 --- a/MediaBrowser.Common/Json/JsonDefaults.cs +++ b/MediaBrowser.Common/Json/JsonDefaults.cs @@ -29,8 +29,14 @@ namespace MediaBrowser.Common.Json NumberHandling = JsonNumberHandling.AllowReadingFromString }; + // Get built-in converters for fallback converting. + var baseNullableInt32Converter = (JsonConverter<int?>)options.GetConverter(typeof(int?)); + var baseNullableInt64Converter = (JsonConverter<long?>)options.GetConverter(typeof(long?)); + options.Converters.Add(new JsonGuidConverter()); options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonNullableStructConverter<int>(baseNullableInt32Converter)); + options.Converters.Add(new JsonNullableStructConverter<long>(baseNullableInt64Converter)); return options; } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index deb674e45..70dcc2397 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -18,8 +18,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.Net.Http.Headers" Version="2.2.8" /> </ItemGroup> @@ -32,6 +33,15 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> <!-- Code analyzers--> diff --git a/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs new file mode 100644 index 000000000..e189d6e70 --- /dev/null +++ b/MediaBrowser.Common/Net/DefaultHttpClientHandler.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Net.Http; + +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Default http client handler. + /// </summary> + public class DefaultHttpClientHandler : HttpClientHandler + { + /// <summary> + /// Initializes a new instance of the <see cref="DefaultHttpClientHandler"/> class. + /// </summary> + public DefaultHttpClientHandler() + { + // TODO change to DecompressionMethods.All with .NET5 + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + } + } +} diff --git a/MediaBrowser.Common/Net/NamedClient.cs b/MediaBrowser.Common/Net/NamedClient.cs new file mode 100644 index 000000000..0f6161c32 --- /dev/null +++ b/MediaBrowser.Common/Net/NamedClient.cs @@ -0,0 +1,18 @@ +namespace MediaBrowser.Common.Net +{ + /// <summary> + /// Registered http client names. + /// </summary> + public static class NamedClient + { + /// <summary> + /// Gets the value for the default named http client. + /// </summary> + public const string Default = nameof(Default); + + /// <summary> + /// Gets the value for the MusicBrainz named http client. + /// </summary> + public const string MusicBrainz = nameof(MusicBrainz); + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 24978d8dd..a5c22e50f 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -2633,6 +2633,7 @@ namespace MediaBrowser.Controller.Entities { return new T { + Path = Path, MetadataCountryCode = GetPreferredMetadataCountryCode(), MetadataLanguage = GetPreferredMetadataLanguage(), Name = GetNameForMetadataLookup(), diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs index 4c2209b67..f9285c768 100644 --- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs +++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs @@ -9,6 +9,12 @@ namespace MediaBrowser.Controller.Extensions public static class ConfigurationExtensions { /// <summary> + /// The key for a setting that specifies the default redirect path + /// to use for requests where the URL base prefix is invalid or missing.. + /// </summary> + public const string DefaultRedirectKey = "DefaultRedirectPath"; + + /// <summary> /// The key for a setting that indicates whether the application should host web client content. /// </summary> public const string HostWebClientKey = "hostwebclient"; diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs index 39b896c0f..9f4c00e1c 100644 --- a/MediaBrowser.Controller/IServerApplicationHost.cs +++ b/MediaBrowser.Controller/IServerApplicationHost.cs @@ -20,6 +20,8 @@ namespace MediaBrowser.Controller IServiceProvider ServiceProvider { get; } + bool CoreStartupHasCompleted { get; } + bool CanLaunchWebBrowser { get; } /// <summary> @@ -117,8 +119,7 @@ namespace MediaBrowser.Controller IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo(); string ExpandVirtualPath(string path); - string ReverseVirtualPath(string path); - Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next); + string ReverseVirtualPath(string path); } } diff --git a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs index d7afd2118..44bd38b54 100644 --- a/MediaBrowser.Controller/LiveTv/ChannelInfo.cs +++ b/MediaBrowser.Controller/LiveTv/ChannelInfo.cs @@ -62,6 +62,7 @@ namespace MediaBrowser.Controller.LiveTv /// </summary> /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> public bool? HasImage { get; set; } + /// <summary> /// Gets or sets a value indicating whether this instance is favorite. /// </summary> diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index df92eda38..9854ec520 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -14,8 +14,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.7" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> </ItemGroup> <ItemGroup> @@ -32,6 +33,15 @@ <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> <!-- Code Analyzers--> diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 550916f82..9f6b2be8f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2090,6 +2090,9 @@ namespace MediaBrowser.Controller.MediaEncoding var hasTextSubs = state.SubtitleStream != null && state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; var hasGraphicalSubs = state.SubtitleStream != null && !state.SubtitleStream.IsTextSubtitleStream && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode; + // If double rate deinterlacing is enabled and the input framerate is 30fps or below, otherwise the output framerate will be too high for many devices + var doubleRateDeinterlace = options.DeinterlaceDoubleRate && (videoStream?.RealFrameRate ?? 60) <= 30; + // When the input may or may not be hardware VAAPI decodable if (isVaapiH264Encoder) { @@ -2136,35 +2139,38 @@ namespace MediaBrowser.Controller.MediaEncoding { if (isVaapiH264Encoder) { - filters.Add(string.Format(CultureInfo.InvariantCulture, "deinterlace_vaapi")); + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "deinterlace_vaapi=rate={0}", + doubleRateDeinterlace ? "field" : "frame")); } } // Add software deinterlace filter before scaling filter - if (state.DeInterlace("h264", true) - || state.DeInterlace("avc", true) - || state.DeInterlace("h265", true) - || state.DeInterlace("hevc", true)) + if ((state.DeInterlace("h264", true) + || state.DeInterlace("avc", true) + || state.DeInterlace("h265", true) + || state.DeInterlace("hevc", true)) + && !isVaapiH264Encoder + && !isQsvH264Encoder + && !isNvdecH264Decoder) { - string deintParam; - var inputFramerate = videoStream?.RealFrameRate; - - // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle - if (string.Equals(options.DeinterlaceMethod, "yadif_bob", StringComparison.OrdinalIgnoreCase) && (inputFramerate ?? 60) <= 30) + if (string.Equals(options.DeinterlaceMethod, "bwdif", StringComparison.OrdinalIgnoreCase)) { - deintParam = "yadif=1:-1:0"; + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "bwdif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } else { - deintParam = "yadif=0:-1:0"; - } - - if (!string.IsNullOrEmpty(deintParam)) - { - if (!isVaapiH264Encoder && !isQsvH264Encoder && !isNvdecH264Decoder) - { - filters.Add(deintParam); - } + filters.Add( + string.Format( + CultureInfo.InvariantCulture, + "yadif={0}:-1:0", + doubleRateDeinterlace ? "1" : "0")); } } @@ -2397,6 +2403,11 @@ namespace MediaBrowser.Controller.MediaEncoding if (state.DeInterlace("h264", true)) { inputModifier += " -deint 1"; + + if (!encodingOptions.DeinterlaceDoubleRate || (videoStream?.RealFrameRate ?? 60) > 30) + { + inputModifier += " -drop_second_field 1"; + } } } } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs index 4cbb63e46..1f3abe8f4 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingJobOptions.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Model.Dlna; -using MediaBrowser.Model.Services; namespace MediaBrowser.Controller.MediaEncoding { @@ -63,26 +62,20 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the id. /// </summary> /// <value>The id.</value> - [ApiMember(Name = "Id", Description = "Item Id", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public Guid Id { get; set; } - [ApiMember(Name = "MediaSourceId", Description = "The media version id, if playing an alternate version", IsRequired = true, DataType = "string", ParameterType = "path", Verb = "GET")] public string MediaSourceId { get; set; } - [ApiMember(Name = "DeviceId", Description = "The device id of the client requesting. Used to stop encoding processes when needed.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string DeviceId { get; set; } - [ApiMember(Name = "Container", Description = "Container", IsRequired = true, DataType = "string", ParameterType = "query", Verb = "GET")] public string Container { get; set; } /// <summary> /// Gets or sets the audio codec. /// </summary> /// <value>The audio codec.</value> - [ApiMember(Name = "AudioCodec", Description = "Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string AudioCodec { get; set; } - [ApiMember(Name = "EnableAutoStreamCopy", Description = "Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool EnableAutoStreamCopy { get; set; } public bool AllowVideoStreamCopy { get; set; } @@ -95,7 +88,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio sample rate. /// </summary> /// <value>The audio sample rate.</value> - [ApiMember(Name = "AudioSampleRate", Description = "Optional. Specify a specific audio sample rate, e.g. 44100", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioSampleRate { get; set; } public int? MaxAudioBitDepth { get; set; } @@ -104,105 +96,86 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the audio bit rate. /// </summary> /// <value>The audio bit rate.</value> - [ApiMember(Name = "AudioBitRate", Description = "Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioBitRate { get; set; } /// <summary> /// Gets or sets the audio channels. /// </summary> /// <value>The audio channels.</value> - [ApiMember(Name = "AudioChannels", Description = "Optional. Specify a specific number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioChannels { get; set; } - [ApiMember(Name = "MaxAudioChannels", Description = "Optional. Specify a maximum number of audio channels to encode to, e.g. 2", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxAudioChannels { get; set; } - [ApiMember(Name = "Static", Description = "Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool Static { get; set; } /// <summary> /// Gets or sets the profile. /// </summary> /// <value>The profile.</value> - [ApiMember(Name = "Profile", Description = "Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Profile { get; set; } /// <summary> /// Gets or sets the level. /// </summary> /// <value>The level.</value> - [ApiMember(Name = "Level", Description = "Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string Level { get; set; } /// <summary> /// Gets or sets the framerate. /// </summary> /// <value>The framerate.</value> - [ApiMember(Name = "Framerate", Description = "Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] public float? Framerate { get; set; } - [ApiMember(Name = "MaxFramerate", Description = "Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.", IsRequired = false, DataType = "double", ParameterType = "query", Verb = "GET")] public float? MaxFramerate { get; set; } - [ApiMember(Name = "CopyTimestamps", Description = "Whether or not to copy timestamps when transcoding with an offset. Defaults to false.", IsRequired = false, DataType = "bool", ParameterType = "query", Verb = "GET")] public bool CopyTimestamps { get; set; } /// <summary> /// Gets or sets the start time ticks. /// </summary> /// <value>The start time ticks.</value> - [ApiMember(Name = "StartTimeTicks", Description = "Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public long? StartTimeTicks { get; set; } /// <summary> /// Gets or sets the width. /// </summary> /// <value>The width.</value> - [ApiMember(Name = "Width", Description = "Optional. The fixed horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Width { get; set; } /// <summary> /// Gets or sets the height. /// </summary> /// <value>The height.</value> - [ApiMember(Name = "Height", Description = "Optional. The fixed vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? Height { get; set; } /// <summary> /// Gets or sets the width of the max. /// </summary> /// <value>The width of the max.</value> - [ApiMember(Name = "MaxWidth", Description = "Optional. The maximum horizontal resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxWidth { get; set; } /// <summary> /// Gets or sets the height of the max. /// </summary> /// <value>The height of the max.</value> - [ApiMember(Name = "MaxHeight", Description = "Optional. The maximum vertical resolution of the encoded video.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxHeight { get; set; } /// <summary> /// Gets or sets the video bit rate. /// </summary> /// <value>The video bit rate.</value> - [ApiMember(Name = "VideoBitRate", Description = "Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? VideoBitRate { get; set; } /// <summary> /// Gets or sets the index of the subtitle stream. /// </summary> /// <value>The index of the subtitle stream.</value> - [ApiMember(Name = "SubtitleStreamIndex", Description = "Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? SubtitleStreamIndex { get; set; } - [ApiMember(Name = "SubtitleMethod", Description = "Optional. Specify the subtitle delivery method.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public SubtitleDeliveryMethod SubtitleMethod { get; set; } - [ApiMember(Name = "MaxRefFrames", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxRefFrames { get; set; } - [ApiMember(Name = "MaxVideoBitDepth", Description = "Optional.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? MaxVideoBitDepth { get; set; } public bool RequireAvc { get; set; } @@ -223,7 +196,6 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the video codec. /// </summary> /// <value>The video codec.</value> - [ApiMember(Name = "VideoCodec", Description = "Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vpx, wmv.", IsRequired = false, DataType = "string", ParameterType = "query", Verb = "GET")] public string VideoCodec { get; set; } public string SubtitleCodec { get; set; } @@ -234,14 +206,12 @@ namespace MediaBrowser.Controller.MediaEncoding /// Gets or sets the index of the audio stream. /// </summary> /// <value>The index of the audio stream.</value> - [ApiMember(Name = "AudioStreamIndex", Description = "Optional. The index of the audio stream to use. If omitted the first audio stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? AudioStreamIndex { get; set; } /// <summary> /// Gets or sets the index of the video stream. /// </summary> /// <value>The index of the video stream.</value> - [ApiMember(Name = "VideoStreamIndex", Description = "Optional. The index of the video stream to use. If omitted the first video stream will be used.", IsRequired = false, DataType = "int", ParameterType = "query", Verb = "GET")] public int? VideoStreamIndex { get; set; } public EncodingContext Context { get; set; } diff --git a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs b/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs deleted file mode 100644 index 1366fd42e..000000000 --- a/MediaBrowser.Controller/Net/AuthenticatedAttribute.cs +++ /dev/null @@ -1,76 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - public class AuthenticatedAttribute : Attribute, IHasRequestFilter, IAuthenticationAttributes - { - public static IAuthService AuthService { get; set; } - - /// <summary> - /// Gets or sets the roles. - /// </summary> - /// <value>The roles.</value> - public string Roles { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [escape parental control]. - /// </summary> - /// <value><c>true</c> if [escape parental control]; otherwise, <c>false</c>.</value> - public bool EscapeParentalControl { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether [allow before startup wizard]. - /// </summary> - /// <value><c>true</c> if [allow before startup wizard]; otherwise, <c>false</c>.</value> - public bool AllowBeforeStartupWizard { get; set; } - - public bool AllowLocal { get; set; } - - /// <summary> - /// The request filter is executed before the service. - /// </summary> - /// <param name="request">The http request wrapper.</param> - /// <param name="response">The http response wrapper.</param> - /// <param name="requestDto">The request DTO.</param> - public void RequestFilter(IRequest request, HttpResponse response, object requestDto) - { - AuthService.Authenticate(request, this); - } - - /// <summary> - /// Order in which Request Filters are executed. - /// <0 Executed before global request filters - /// >0 Executed after global request filters - /// </summary> - /// <value>The priority.</value> - public int Priority => 0; - - public string[] GetRoles() - { - return (Roles ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - } - - public bool IgnoreLegacyAuth { get; set; } - - public bool AllowLocalOnly { get; set; } - } - - public interface IAuthenticationAttributes - { - bool EscapeParentalControl { get; } - - bool AllowBeforeStartupWizard { get; } - - bool AllowLocal { get; } - - bool AllowLocalOnly { get; } - - string[] GetRoles(); - - bool IgnoreLegacyAuth { get; } - } -} diff --git a/MediaBrowser.Controller/Net/IAuthService.cs b/MediaBrowser.Controller/Net/IAuthService.cs index 2055a656a..04b2e13e8 100644 --- a/MediaBrowser.Controller/Net/IAuthService.cs +++ b/MediaBrowser.Controller/Net/IAuthService.cs @@ -1,7 +1,5 @@ #nullable enable -using Jellyfin.Data.Entities; -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -12,21 +10,6 @@ namespace MediaBrowser.Controller.Net public interface IAuthService { /// <summary> - /// Authenticate and authorize request. - /// </summary> - /// <param name="request">Request.</param> - /// <param name="authAttribtutes">Authorization attributes.</param> - void Authenticate(IRequest request, IAuthenticationAttributes authAttribtutes); - - /// <summary> - /// Authenticate and authorize request. - /// </summary> - /// <param name="request">Request.</param> - /// <param name="authAttribtutes">Authorization attributes.</param> - /// <returns>Authenticated user.</returns> - User? Authenticate(HttpRequest request, IAuthenticationAttributes authAttribtutes); - - /// <summary> /// Authenticate request. /// </summary> /// <param name="request">The request.</param> diff --git a/MediaBrowser.Controller/Net/IAuthorizationContext.cs b/MediaBrowser.Controller/Net/IAuthorizationContext.cs index 37a7425b9..0d310548d 100644 --- a/MediaBrowser.Controller/Net/IAuthorizationContext.cs +++ b/MediaBrowser.Controller/Net/IAuthorizationContext.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net @@ -13,14 +12,7 @@ namespace MediaBrowser.Controller.Net /// </summary> /// <param name="requestContext">The request context.</param> /// <returns>AuthorizationInfo.</returns> - AuthorizationInfo GetAuthorizationInfo(object requestContext); - - /// <summary> - /// Gets the authorization information. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <returns>AuthorizationInfo.</returns> - AuthorizationInfo GetAuthorizationInfo(IRequest requestContext); + AuthorizationInfo GetAuthorizationInfo(HttpContext requestContext); /// <summary> /// Gets the authorization information. diff --git a/MediaBrowser.Controller/Net/IHasResultFactory.cs b/MediaBrowser.Controller/Net/IHasResultFactory.cs deleted file mode 100644 index b8cf8cd78..000000000 --- a/MediaBrowser.Controller/Net/IHasResultFactory.cs +++ /dev/null @@ -1,17 +0,0 @@ -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHasResultFactory - /// Services that require a ResultFactory should implement this - /// </summary> - public interface IHasResultFactory : IRequiresRequest - { - /// <summary> - /// Gets or sets the result factory. - /// </summary> - /// <value>The result factory.</value> - IHttpResultFactory ResultFactory { get; set; } - } -} diff --git a/MediaBrowser.Controller/Net/IHttpResultFactory.cs b/MediaBrowser.Controller/Net/IHttpResultFactory.cs deleted file mode 100644 index 8293a8714..000000000 --- a/MediaBrowser.Controller/Net/IHttpResultFactory.cs +++ /dev/null @@ -1,82 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Model.Services; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHttpResultFactory. - /// </summary> - public interface IHttpResultFactory - { - /// <summary> - /// Gets the result. - /// </summary> - /// <param name="content">The content.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <returns>System.Object.</returns> - object GetResult(string content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetResult(IRequest requestContext, string content, string contentType, IDictionary<string, string> responseHeaders = null); - - object GetRedirectResult(string url); - - object GetResult<T>(IRequest requestContext, T result, IDictionary<string, string> responseHeaders = null) - where T : class; - - /// <summary> - /// Gets the static result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="cacheKey">The cache key.</param> - /// <param name="lastDateModified">The last date modified.</param> - /// <param name="cacheDuration">Duration of the cache.</param> - /// <param name="contentType">Type of the content.</param> - /// <param name="factoryFn">The factory fn.</param> - /// <param name="responseHeaders">The response headers.</param> - /// <param name="isHeadRequest">if set to <c>true</c> [is head request].</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticResult(IRequest requestContext, - Guid cacheKey, - DateTime? lastDateModified, - TimeSpan? cacheDuration, - string contentType, Func<Task<Stream>> factoryFn, - IDictionary<string, string> responseHeaders = null, - bool isHeadRequest = false); - - /// <summary> - /// Gets the static result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="options">The options.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticResult(IRequest requestContext, StaticResultOptions options); - - /// <summary> - /// Gets the static file result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="path">The path.</param> - /// <param name="fileShare">The file share.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, string path, FileShare fileShare = FileShare.Read); - - /// <summary> - /// Gets the static file result. - /// </summary> - /// <param name="requestContext">The request context.</param> - /// <param name="options">The options.</param> - /// <returns>System.Object.</returns> - Task<object> GetStaticFileResult(IRequest requestContext, - StaticFileResultOptions options); - } -} diff --git a/MediaBrowser.Controller/Net/IHttpServer.cs b/MediaBrowser.Controller/Net/IHttpServer.cs deleted file mode 100644 index b04ebda8c..000000000 --- a/MediaBrowser.Controller/Net/IHttpServer.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Jellyfin.Data.Events; -using MediaBrowser.Model.Services; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Controller.Net -{ - /// <summary> - /// Interface IHttpServer. - /// </summary> - public interface IHttpServer - { - /// <summary> - /// Gets the URL prefix. - /// </summary> - /// <value>The URL prefix.</value> - string[] UrlPrefixes { get; } - - /// <summary> - /// Occurs when [web socket connected]. - /// </summary> - event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; - - /// <summary> - /// Inits this instance. - /// </summary> - void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listener, IEnumerable<string> urlPrefixes); - - /// <summary> - /// If set, all requests will respond with this message. - /// </summary> - string GlobalResponse { get; set; } - - /// <summary> - /// The HTTP request handler. - /// </summary> - /// <param name="context"></param> - /// <returns></returns> - Task RequestHandler(HttpContext context); - - /// <summary> - /// Get the default CORS headers. - /// </summary> - /// <param name="req"></param> - /// <returns></returns> - IDictionary<string, string> GetDefaultCorsHeaders(IRequest req); - } -} diff --git a/MediaBrowser.Controller/Net/ISessionContext.cs b/MediaBrowser.Controller/Net/ISessionContext.cs index 5da748f41..a60dc2ea1 100644 --- a/MediaBrowser.Controller/Net/ISessionContext.cs +++ b/MediaBrowser.Controller/Net/ISessionContext.cs @@ -2,7 +2,7 @@ using Jellyfin.Data.Entities; using MediaBrowser.Controller.Session; -using MediaBrowser.Model.Services; +using Microsoft.AspNetCore.Http; namespace MediaBrowser.Controller.Net { @@ -12,8 +12,8 @@ namespace MediaBrowser.Controller.Net User GetUser(object requestContext); - SessionInfo GetSession(IRequest requestContext); + SessionInfo GetSession(HttpContext requestContext); - User GetUser(IRequest requestContext); + User GetUser(HttpContext requestContext); } } diff --git a/MediaBrowser.Controller/Net/IWebSocketManager.cs b/MediaBrowser.Controller/Net/IWebSocketManager.cs new file mode 100644 index 000000000..e9f00ae88 --- /dev/null +++ b/MediaBrowser.Controller/Net/IWebSocketManager.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Jellyfin.Data.Events; +using Microsoft.AspNetCore.Http; + +namespace MediaBrowser.Controller.Net +{ + /// <summary> + /// Interface IHttpServer. + /// </summary> + public interface IWebSocketManager + { + /// <summary> + /// Occurs when [web socket connected]. + /// </summary> + event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected; + + /// <summary> + /// Inits this instance. + /// </summary> + /// <param name="listeners">The websocket listeners.</param> + void Init(IEnumerable<IWebSocketListener> listeners); + + /// <summary> + /// The HTTP request handler. + /// </summary> + /// <param name="context">The current HTTP context.</param> + /// <returns>The task.</returns> + Task WebSocketRequestHandler(HttpContext context); + } +} diff --git a/MediaBrowser.Controller/Net/StaticResultOptions.cs b/MediaBrowser.Controller/Net/StaticResultOptions.cs deleted file mode 100644 index c1e9bc845..000000000 --- a/MediaBrowser.Controller/Net/StaticResultOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.Net -{ - public class StaticResultOptions - { - public string ContentType { get; set; } - - public TimeSpan? CacheDuration { get; set; } - - public DateTime? DateLastModified { get; set; } - - public Func<Task<Stream>> ContentFactory { get; set; } - - public bool IsHeadRequest { get; set; } - - public IDictionary<string, string> ResponseHeaders { get; set; } - - public Action OnComplete { get; set; } - - public Action OnError { get; set; } - - public string Path { get; set; } - - public long? ContentLength { get; set; } - - public FileShare FileShare { get; set; } - - public StaticResultOptions() - { - ResponseHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - FileShare = FileShare.Read; - } - } - - public class StaticFileResultOptions : StaticResultOptions - { - } -} diff --git a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs index 49974c2a3..b777cc1d3 100644 --- a/MediaBrowser.Controller/Providers/ItemLookupInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemLookupInfo.cs @@ -15,6 +15,12 @@ namespace MediaBrowser.Controller.Providers public string Name { get; set; } /// <summary> + /// Gets or sets the path. + /// </summary> + /// <value>The path.</value> + public string Path { get; set; } + + /// <summary> /// Gets or sets the metadata language. /// </summary> /// <value>The metadata language.</value> diff --git a/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs new file mode 100644 index 000000000..959a2d771 --- /dev/null +++ b/MediaBrowser.Controller/QuickConnect/IQuickConnect.cs @@ -0,0 +1,87 @@ +using System; +using MediaBrowser.Model.QuickConnect; + +namespace MediaBrowser.Controller.QuickConnect +{ + /// <summary> + /// Quick connect standard interface. + /// </summary> + public interface IQuickConnect + { + /// <summary> + /// Gets or sets the length of user facing codes. + /// </summary> + int CodeLength { get; set; } + + /// <summary> + /// Gets or sets the name of internal access tokens. + /// </summary> + string TokenName { get; set; } + + /// <summary> + /// Gets the current state of quick connect. + /// </summary> + QuickConnectState State { get; } + + /// <summary> + /// Gets or sets the time (in minutes) before quick connect will automatically deactivate. + /// </summary> + int Timeout { get; set; } + + /// <summary> + /// Assert that quick connect is currently active and throws an exception if it is not. + /// </summary> + void AssertActive(); + + /// <summary> + /// Temporarily activates quick connect for a short amount of time. + /// </summary> + void Activate(); + + /// <summary> + /// Changes the state of quick connect. + /// </summary> + /// <param name="newState">New state to change to.</param> + void SetState(QuickConnectState newState); + + /// <summary> + /// Initiates a new quick connect request. + /// </summary> + /// <returns>A quick connect result with tokens to proceed or throws an exception if not active.</returns> + QuickConnectResult TryConnect(); + + /// <summary> + /// Checks the status of an individual request. + /// </summary> + /// <param name="secret">Unique secret identifier of the request.</param> + /// <returns>Quick connect result.</returns> + QuickConnectResult CheckRequestStatus(string secret); + + /// <summary> + /// Authorizes a quick connect request to connect as the calling user. + /// </summary> + /// <param name="userId">User id.</param> + /// <param name="code">Identifying code for the request.</param> + /// <returns>A boolean indicating if the authorization completed successfully.</returns> + bool AuthorizeRequest(Guid userId, string code); + + /// <summary> + /// Expire quick connect requests that are over the time limit. If <paramref name="expireAll"/> is true, all requests are unconditionally expired. + /// </summary> + /// <param name="expireAll">If true, all requests will be expired.</param> + void ExpireRequests(bool expireAll = false); + + /// <summary> + /// Deletes all quick connect access tokens for the provided user. + /// </summary> + /// <param name="user">Guid of the user to delete tokens for.</param> + /// <returns>A count of the deleted tokens.</returns> + int DeleteAllDevices(Guid user); + + /// <summary> + /// Generates a short code to display to the user to uniquely identify this request. + /// </summary> + /// <returns>A short, unique alphanumeric string.</returns> + string GenerateCode(); + } +} diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 9237d21df..228b2331d 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -267,6 +267,14 @@ namespace MediaBrowser.Controller.Session Task<AuthenticationResult> AuthenticateNewSession(AuthenticationRequest request); /// <summary> + /// Authenticates a new session with quick connect. + /// </summary> + /// <param name="request">The request.</param> + /// <param name="token">Quick connect access token.</param> + /// <returns>Task{SessionInfo}.</returns> + Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token); + + /// <summary> /// Creates the new session. /// </summary> /// <param name="request">The request.</param> diff --git a/MediaBrowser.Model/Configuration/EncodingOptions.cs b/MediaBrowser.Model/Configuration/EncodingOptions.cs index 9a30f7e9f..41ff517a0 100644 --- a/MediaBrowser.Model/Configuration/EncodingOptions.cs +++ b/MediaBrowser.Model/Configuration/EncodingOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration public double DownMixAudioBoost { get; set; } + public int MaxMuxingQueueSize { get; set; } + public bool EnableThrottling { get; set; } public int ThrottleDelaySeconds { get; set; } @@ -35,6 +37,8 @@ namespace MediaBrowser.Model.Configuration public string EncoderPreset { get; set; } + public bool DeinterlaceDoubleRate { get; set; } + public string DeinterlaceMethod { get; set; } public bool EnableDecodingColorDepth10Hevc { get; set; } @@ -50,6 +54,7 @@ namespace MediaBrowser.Model.Configuration public EncodingOptions() { DownMixAudioBoost = 2; + MaxMuxingQueueSize = 2048; EnableThrottling = false; ThrottleDelaySeconds = 180; EncodingThreadCount = -1; @@ -57,6 +62,7 @@ namespace MediaBrowser.Model.Configuration VaapiDevice = "/dev/dri/renderD128"; H264Crf = 23; H265Crf = 28; + DeinterlaceDoubleRate = false; DeinterlaceMethod = "yadif"; EnableDecodingColorDepth10Hevc = true; EnableDecodingColorDepth10Vp9 = true; diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index c66091f9d..33975bc1e 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -78,6 +78,11 @@ namespace MediaBrowser.Model.Configuration /// <value><c>true</c> if this instance is port authorized; otherwise, <c>false</c>.</value> public bool IsPortAuthorized { get; set; } + /// <summary> + /// Gets or sets if quick connect is available for use on this server. + /// </summary> + public bool QuickConnectAvailable { get; set; } + public bool AutoRunWebApp { get; set; } public bool EnableRemoteAccess { get; set; } @@ -299,6 +304,7 @@ namespace MediaBrowser.Model.Configuration AutoRunWebApp = true; EnableRemoteAccess = true; + QuickConnectAvailable = false; EnableUPnP = false; MinResumePct = 5; diff --git a/MediaBrowser.Model/IO/FileSystemMetadata.cs b/MediaBrowser.Model/IO/FileSystemMetadata.cs index b23119d08..118c78e80 100644 --- a/MediaBrowser.Model/IO/FileSystemMetadata.cs +++ b/MediaBrowser.Model/IO/FileSystemMetadata.cs @@ -56,7 +56,7 @@ namespace MediaBrowser.Model.IO public DateTime CreationTimeUtc { get; set; } /// <summary> - /// Gets a value indicating whether this instance is directory. + /// Gets or sets a value indicating whether this instance is directory. /// </summary> /// <value><c>true</c> if this instance is directory; otherwise, <c>false</c>.</value> public bool IsDirectory { get; set; } diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs index bba69d4b4..dc6549787 100644 --- a/MediaBrowser.Model/IO/IFileSystem.cs +++ b/MediaBrowser.Model/IO/IFileSystem.cs @@ -201,9 +201,9 @@ namespace MediaBrowser.Model.IO IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false); void SetHidden(string path, bool isHidden); - void SetReadOnly(string path, bool readOnly); + void SetAttributes(string path, bool isHidden, bool readOnly); + List<FileSystemMetadata> GetDrives(); - void SetExecutable(string path); } } diff --git a/MediaBrowser.Model/IO/IShortcutHandler.cs b/MediaBrowser.Model/IO/IShortcutHandler.cs index 5c663aa0d..14d5c4b62 100644 --- a/MediaBrowser.Model/IO/IShortcutHandler.cs +++ b/MediaBrowser.Model/IO/IShortcutHandler.cs @@ -22,7 +22,6 @@ namespace MediaBrowser.Model.IO /// </summary> /// <param name="shortcutPath">The shortcut path.</param> /// <param name="targetPath">The target path.</param> - /// <returns>System.String.</returns> void Create(string shortcutPath, string targetPath); } } diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index af5ba5b17..0e09db16e 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken); - Task<int> CopyToAsync(Stream source, Stream destination, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken); Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken); diff --git a/MediaBrowser.Model/IO/IZipClient.cs b/MediaBrowser.Model/IO/IZipClient.cs index 2daa54f22..fca52ebae 100644 --- a/MediaBrowser.Model/IO/IZipClient.cs +++ b/MediaBrowser.Model/IO/IZipClient.cs @@ -26,6 +26,7 @@ namespace MediaBrowser.Model.IO void ExtractAll(Stream source, string targetPath, bool overwriteExistingFiles); void ExtractAllFromGz(Stream source, string targetPath, bool overwriteExistingFiles); + void ExtractFirstFileFromGz(Stream source, string targetPath, string defaultFileName); /// <summary> diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 0491c9072..c0a75009a 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -20,11 +20,21 @@ <TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release' ">true</TreatWarningsAsErrors> <Nullable>enable</Nullable> <LangVersion>latest</LangVersion> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + </PropertyGroup> + + <PropertyGroup Condition=" '$(Stability)'=='Unstable'"> + <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> </PropertyGroup> <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/> <PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.7" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> <PackageReference Include="System.Text.Json" Version="5.0.0-preview.8.20407.11" /> </ItemGroup> diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs new file mode 100644 index 000000000..0fa40b6a7 --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectResult.cs @@ -0,0 +1,40 @@ +using System; + +namespace MediaBrowser.Model.QuickConnect +{ + /// <summary> + /// Stores the result of an incoming quick connect request. + /// </summary> + public class QuickConnectResult + { + /// <summary> + /// Gets a value indicating whether this request is authorized. + /// </summary> + public bool Authenticated => !string.IsNullOrEmpty(Authentication); + + /// <summary> + /// Gets or sets the secret value used to uniquely identify this request. Can be used to retrieve authentication information. + /// </summary> + public string? Secret { get; set; } + + /// <summary> + /// Gets or sets the user facing code used so the user can quickly differentiate this request from others. + /// </summary> + public string? Code { get; set; } + + /// <summary> + /// Gets or sets the private access token. + /// </summary> + public string? Authentication { get; set; } + + /// <summary> + /// Gets or sets an error message. + /// </summary> + public string? Error { get; set; } + + /// <summary> + /// Gets or sets the DateTime that this request was created. + /// </summary> + public DateTime? DateAdded { get; set; } + } +} diff --git a/MediaBrowser.Model/QuickConnect/QuickConnectState.cs b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs new file mode 100644 index 000000000..f1074f25f --- /dev/null +++ b/MediaBrowser.Model/QuickConnect/QuickConnectState.cs @@ -0,0 +1,23 @@ +namespace MediaBrowser.Model.QuickConnect +{ + /// <summary> + /// Quick connect state. + /// </summary> + public enum QuickConnectState + { + /// <summary> + /// This feature has not been opted into and is unavailable until the server administrator chooses to opt-in. + /// </summary> + Unavailable = 0, + + /// <summary> + /// The feature is enabled for use on the server but is not currently accepting connection requests. + /// </summary> + Available = 1, + + /// <summary> + /// The feature is actively accepting connection requests. + /// </summary> + Active = 2 + } +} diff --git a/MediaBrowser.Model/Services/ApiMemberAttribute.cs b/MediaBrowser.Model/Services/ApiMemberAttribute.cs deleted file mode 100644 index 63f3ecd55..000000000 --- a/MediaBrowser.Model/Services/ApiMemberAttribute.cs +++ /dev/null @@ -1,65 +0,0 @@ -#nullable disable -using System; - -namespace MediaBrowser.Model.Services -{ - /// <summary> - /// Identifies a single API endpoint. - /// </summary> - [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] - public class ApiMemberAttribute : Attribute - { - /// <summary> - /// Gets or sets verb to which applies attribute. By default applies to all verbs. - /// </summary> - public string Verb { get; set; } - - /// <summary> - /// Gets or sets parameter type: It can be only one of the following: path, query, body, form, or header. - /// </summary> - public string ParameterType { get; set; } - - /// <summary> - /// Gets or sets unique name for the parameter. Each name must be unique, even if they are associated with different paramType values. - /// </summary> - /// <remarks> - /// <para> - /// Other notes on the name field: - /// If paramType is body, the name is used only for UI and codegeneration. - /// If paramType is path, the name field must correspond to the associated path segment from the path field in the api object. - /// If paramType is query, the name field corresponds to the query param name. - /// </para> - /// </remarks> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the human-readable description for the parameter. - /// </summary> - public string Description { get; set; } - - /// <summary> - /// For path, query, and header paramTypes, this field must be a primitive. For body, this can be a complex or container datatype. - /// </summary> - public string DataType { get; set; } - - /// <summary> - /// For path, this is always true. Otherwise, this field tells the client whether or not the field must be supplied. - /// </summary> - public bool IsRequired { get; set; } - - /// <summary> - /// For query params, this specifies that a comma-separated list of values can be passed to the API. For path and body types, this field cannot be true. - /// </summary> - public bool AllowMultiple { get; set; } - - /// <summary> - /// Gets or sets route to which applies attribute, matches using StartsWith. By default applies to all routes. - /// </summary> - public string Route { get; set; } - - /// <summary> - /// Whether to exclude this property from being included in the ModelSchema. - /// </summary> - public bool ExcludeInSchema { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs b/MediaBrowser.Model/Services/IAsyncStreamWriter.cs deleted file mode 100644 index afbca78a2..000000000 --- a/MediaBrowser.Model/Services/IAsyncStreamWriter.cs +++ /dev/null @@ -1,13 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Model.Services -{ - public interface IAsyncStreamWriter - { - Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken); - } -} diff --git a/MediaBrowser.Model/Services/IHasHeaders.cs b/MediaBrowser.Model/Services/IHasHeaders.cs deleted file mode 100644 index 313f34b41..000000000 --- a/MediaBrowser.Model/Services/IHasHeaders.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.Collections.Generic; - -namespace MediaBrowser.Model.Services -{ - public interface IHasHeaders - { - IDictionary<string, string> Headers { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHasRequestFilter.cs b/MediaBrowser.Model/Services/IHasRequestFilter.cs deleted file mode 100644 index b83d3b075..000000000 --- a/MediaBrowser.Model/Services/IHasRequestFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -#pragma warning disable CS1591 - -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IHasRequestFilter - { - /// <summary> - /// Gets the order in which Request Filters are executed. - /// <0 Executed before global request filters. - /// >0 Executed after global request filters. - /// </summary> - int Priority { get; } - - /// <summary> - /// The request filter is executed before the service. - /// </summary> - /// <param name="req">The http request wrapper.</param> - /// <param name="res">The http response wrapper.</param> - /// <param name="requestDto">The request DTO.</param> - void RequestFilter(IRequest req, HttpResponse res, object requestDto); - } -} diff --git a/MediaBrowser.Model/Services/IHttpRequest.cs b/MediaBrowser.Model/Services/IHttpRequest.cs deleted file mode 100644 index 3ea65195c..000000000 --- a/MediaBrowser.Model/Services/IHttpRequest.cs +++ /dev/null @@ -1,17 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - public interface IHttpRequest : IRequest - { - /// <summary> - /// Gets the HTTP Verb. - /// </summary> - string HttpMethod { get; } - - /// <summary> - /// Gets the value of the Accept HTTP Request Header. - /// </summary> - string Accept { get; } - } -} diff --git a/MediaBrowser.Model/Services/IHttpResult.cs b/MediaBrowser.Model/Services/IHttpResult.cs deleted file mode 100644 index abc581d8e..000000000 --- a/MediaBrowser.Model/Services/IHttpResult.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Net; - -namespace MediaBrowser.Model.Services -{ - public interface IHttpResult : IHasHeaders - { - /// <summary> - /// The HTTP Response Status. - /// </summary> - int Status { get; set; } - - /// <summary> - /// The HTTP Response Status Code. - /// </summary> - HttpStatusCode StatusCode { get; set; } - - /// <summary> - /// The HTTP Response ContentType. - /// </summary> - string ContentType { get; set; } - - /// <summary> - /// Response DTO. - /// </summary> - object Response { get; set; } - - /// <summary> - /// Holds the request call context. - /// </summary> - IRequest RequestContext { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IRequest.cs b/MediaBrowser.Model/Services/IRequest.cs deleted file mode 100644 index 8bc1d3668..000000000 --- a/MediaBrowser.Model/Services/IRequest.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.IO; -using Microsoft.AspNetCore.Http; - -namespace MediaBrowser.Model.Services -{ - public interface IRequest - { - HttpResponse Response { get; } - - /// <summary> - /// The name of the service being called (e.g. Request DTO Name) - /// </summary> - string OperationName { get; set; } - - /// <summary> - /// The Verb / HttpMethod or Action for this request - /// </summary> - string Verb { get; } - - /// <summary> - /// The request ContentType. - /// </summary> - string ContentType { get; } - - bool IsLocal { get; } - - string UserAgent { get; } - - /// <summary> - /// The expected Response ContentType for this request. - /// </summary> - string ResponseContentType { get; set; } - - /// <summary> - /// Attach any data to this request that all filters and services can access. - /// </summary> - Dictionary<string, object> Items { get; } - - IHeaderDictionary Headers { get; } - - IQueryCollection QueryString { get; } - - string RawUrl { get; } - - string AbsoluteUri { get; } - - /// <summary> - /// The Remote Ip as reported by X-Forwarded-For, X-Real-IP or Request.UserHostAddress - /// </summary> - string RemoteIp { get; } - - /// <summary> - /// The value of the Authorization Header used to send the Api Key, null if not available. - /// </summary> - string Authorization { get; } - - string[] AcceptTypes { get; } - - string PathInfo { get; } - - Stream InputStream { get; } - - long ContentLength { get; } - - /// <summary> - /// The value of the Referrer, null if not available. - /// </summary> - Uri UrlReferrer { get; } - } - - public interface IHttpFile - { - string Name { get; } - - string FileName { get; } - - long ContentLength { get; } - - string ContentType { get; } - - Stream InputStream { get; } - } - - public interface IRequiresRequest - { - IRequest Request { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IRequiresRequestStream.cs b/MediaBrowser.Model/Services/IRequiresRequestStream.cs deleted file mode 100644 index 3e5f2da42..000000000 --- a/MediaBrowser.Model/Services/IRequiresRequestStream.cs +++ /dev/null @@ -1,14 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace MediaBrowser.Model.Services -{ - public interface IRequiresRequestStream - { - /// <summary> - /// The raw Http Request Input Stream. - /// </summary> - Stream RequestStream { get; set; } - } -} diff --git a/MediaBrowser.Model/Services/IService.cs b/MediaBrowser.Model/Services/IService.cs deleted file mode 100644 index 5233f57ab..000000000 --- a/MediaBrowser.Model/Services/IService.cs +++ /dev/null @@ -1,15 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Services -{ - // marker interface - public interface IService - { - } - - public interface IReturn { } - - public interface IReturn<T> : IReturn { } - - public interface IReturnVoid : IReturn { } -} diff --git a/MediaBrowser.Model/Services/IStreamWriter.cs b/MediaBrowser.Model/Services/IStreamWriter.cs deleted file mode 100644 index 3ebfef66b..000000000 --- a/MediaBrowser.Model/Services/IStreamWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -using System.IO; - -namespace MediaBrowser.Model.Services -{ - public interface IStreamWriter - { - void WriteTo(Stream responseStream); - } -} diff --git a/MediaBrowser.Model/Services/QueryParamCollection.cs b/MediaBrowser.Model/Services/QueryParamCollection.cs deleted file mode 100644 index bdb0cabdf..000000000 --- a/MediaBrowser.Model/Services/QueryParamCollection.cs +++ /dev/null @@ -1,147 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Dto; - -namespace MediaBrowser.Model.Services -{ - // Remove this garbage class, it's just a bastard copy of NameValueCollection - public class QueryParamCollection : List<NameValuePair> - { - public QueryParamCollection() - { - } - - private static StringComparison GetStringComparison() - { - return StringComparison.OrdinalIgnoreCase; - } - - /// <summary> - /// Adds a new query parameter. - /// </summary> - public void Add(string key, string value) - { - Add(new NameValuePair(key, value)); - } - - private void Set(string key, string value) - { - if (string.IsNullOrEmpty(value)) - { - var parameters = GetItems(key); - - foreach (var p in parameters) - { - Remove(p); - } - - return; - } - - foreach (var pair in this) - { - var stringComparison = GetStringComparison(); - - if (string.Equals(key, pair.Name, stringComparison)) - { - pair.Value = value; - return; - } - } - - Add(key, value); - } - - private string Get(string name) - { - var stringComparison = GetStringComparison(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - return pair.Value; - } - } - - return null; - } - - private List<NameValuePair> GetItems(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List<NameValuePair>(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair); - } - } - - return list; - } - - public virtual List<string> GetValues(string name) - { - var stringComparison = GetStringComparison(); - - var list = new List<string>(); - - foreach (var pair in this) - { - if (string.Equals(pair.Name, name, stringComparison)) - { - list.Add(pair.Value); - } - } - - return list; - } - - public IEnumerable<string> Keys - { - get - { - var keys = new string[this.Count]; - - for (var i = 0; i < keys.Length; i++) - { - keys[i] = this[i].Name; - } - - return keys; - } - } - - /// <summary> - /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name - /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting. - /// </summary> - /// <param name="name">The query parameter name.</param> - /// <returns>The query parameter value or array of values.</returns> - public string this[string name] - { - get => Get(name); - set => Set(name, value); - } - - private string GetQueryStringValue(NameValuePair pair) - { - return pair.Name + "=" + pair.Value; - } - - public override string ToString() - { - var vals = this.Select(GetQueryStringValue).ToArray(); - - return string.Join("&", vals); - } - } -} diff --git a/MediaBrowser.Model/Services/RouteAttribute.cs b/MediaBrowser.Model/Services/RouteAttribute.cs deleted file mode 100644 index f8bf51112..000000000 --- a/MediaBrowser.Model/Services/RouteAttribute.cs +++ /dev/null @@ -1,163 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Services -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class RouteAttribute : Attribute - { - /// <summary> - /// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para> - /// </summary> - /// <param name="path"> - /// <para>The path template to map to the request. See - /// <see cref="Path">RouteAttribute.Path</see> - /// for details on the correct format.</para> - /// </param> - public RouteAttribute(string path) - : this(path, null) - { - } - - /// <summary> - /// <para>Initializes an instance of the <see cref="RouteAttribute"/> class.</para> - /// </summary> - /// <param name="path"> - /// <para>The path template to map to the request. See - /// <see cref="Path">RouteAttribute.Path</see> - /// for details on the correct format.</para> - /// </param> - /// <param name="verbs">A comma-delimited list of HTTP verbs supported by the - /// service. If unspecified, all verbs are assumed to be supported.</param> - public RouteAttribute(string path, string verbs) - { - Path = path; - Verbs = verbs; - } - - /// <summary> - /// Gets or sets the path template to be mapped to the request. - /// </summary> - /// <value> - /// A <see cref="String"/> value providing the path mapped to - /// the request. Never <see langword="null"/>. - /// </value> - /// <remarks> - /// <para>Some examples of valid paths are:</para> - /// - /// <list> - /// <item>"/Inventory"</item> - /// <item>"/Inventory/{Category}/{ItemId}"</item> - /// <item>"/Inventory/{ItemPath*}"</item> - /// </list> - /// - /// <para>Variables are specified within "{}" - /// brackets. Each variable in the path is mapped to the same-named property - /// on the request DTO. At runtime, ServiceStack will parse the - /// request URL, extract the variable values, instantiate the request DTO, - /// and assign the variable values into the corresponding request properties, - /// prior to passing the request DTO to the service object for processing.</para> - /// - /// <para>It is not necessary to specify all request properties as - /// variables in the path. For unspecified properties, callers may provide - /// values in the query string. For example: the URL - /// "http://services/Inventory?Category=Books&ItemId=12345" causes the same - /// request DTO to be processed as "http://services/Inventory/Books/12345", - /// provided that the paths "/Inventory" (which supports the first URL) and - /// "/Inventory/{Category}/{ItemId}" (which supports the second URL) - /// are both mapped to the request DTO.</para> - /// - /// <para>Please note that while it is possible to specify property values - /// in the query string, it is generally considered to be less RESTful and - /// less desirable than to specify them as variables in the path. Using the - /// query string to specify property values may also interfere with HTTP - /// caching.</para> - /// - /// <para>The final variable in the path may contain a "*" suffix - /// to grab all remaining segments in the path portion of the request URL and assign - /// them to a single property on the request DTO. - /// For example, if the path "/Inventory/{ItemPath*}" is mapped to the request DTO, - /// then the request URL "http://services/Inventory/Books/12345" will result - /// in a request DTO whose ItemPath property contains "Books/12345". - /// You may only specify one such variable in the path, and it must be positioned at - /// the end of the path.</para> - /// </remarks> - public string Path { get; set; } - - /// <summary> - /// Gets or sets short summary of what the route does. - /// </summary> - public string Summary { get; set; } - - public string Description { get; set; } - - public bool IsHidden { get; set; } - - /// <summary> - /// Gets or sets longer text to explain the behaviour of the route. - /// </summary> - public string Notes { get; set; } - - /// <summary> - /// Gets or sets a comma-delimited list of HTTP verbs supported by the service, such as - /// "GET,PUT,POST,DELETE". - /// </summary> - /// <value> - /// A <see cref="String"/> providing a comma-delimited list of HTTP verbs supported - /// by the service, <see langword="null"/> or empty if all verbs are supported. - /// </value> - public string Verbs { get; set; } - - /// <summary> - /// Used to rank the precedences of route definitions in reverse routing. - /// i.e. Priorities below 0 are auto-generated have less precedence. - /// </summary> - public int Priority { get; set; } - - protected bool Equals(RouteAttribute other) - { - return base.Equals(other) - && string.Equals(Path, other.Path) - && string.Equals(Summary, other.Summary) - && string.Equals(Notes, other.Notes) - && string.Equals(Verbs, other.Verbs) - && Priority == other.Priority; - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return Equals((RouteAttribute)obj); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = (hashCode * 397) ^ (Path != null ? Path.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Summary != null ? Summary.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Notes != null ? Notes.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ (Verbs != null ? Verbs.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ Priority; - return hashCode; - } - } - } -} diff --git a/MediaBrowser.Model/Session/PlayRequest.cs b/MediaBrowser.Model/Session/PlayRequest.cs index eeb25c2e8..6a66465a2 100644 --- a/MediaBrowser.Model/Session/PlayRequest.cs +++ b/MediaBrowser.Model/Session/PlayRequest.cs @@ -2,7 +2,6 @@ #pragma warning disable CS1591 using System; -using MediaBrowser.Model.Services; namespace MediaBrowser.Model.Session { diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 85966b3bf..39f93c479 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,9 +16,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.6" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.6" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.7" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="3.1.7" /> <PackageReference Include="OptimizedPriorityQueue" Version="4.2.0" /> <PackageReference Include="PlaylistsNET" Version="1.1.2" /> <PackageReference Include="TvDbSharper" Version="3.2.1" /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs index f31a7faea..291b36027 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonImageProvider.cs @@ -31,9 +31,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People _httpClientFactory = httpClientFactory; } + public static string ProviderName => TmdbUtils.ProviderName; + + /// <inheritdoc /> public string Name => ProviderName; - public static string ProviderName => TmdbUtils.ProviderName; + /// <inheritdoc /> + public int Order => 0; public bool Supports(BaseItem item) { @@ -125,8 +129,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return profile.Iso_639_1?.ToString(); } - public int Order => 0; - public Task<HttpResponseMessage> GetImageResponse(string url, CancellationToken cancellationToken) { return _httpClientFactory.CreateClient().GetAsync(url, cancellationToken); diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index e9fb5c703..a4b1387d3 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -32,7 +32,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People { const string DataFileName = "info.json"; - internal static TmdbPersonProvider Current { get; private set; } + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; @@ -55,6 +55,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People Current = this; } + internal static TmdbPersonProvider Current { get; private set; } + public string Name => TmdbUtils.ProviderName; public async Task<IEnumerable<RemoteSearchResult>> GetSearchResults(PersonLookupInfo searchInfo, CancellationToken cancellationToken) @@ -95,7 +97,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return new List<RemoteSearchResult>(); } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", WebUtility.UrlEncode(searchInfo.Name), TmdbUtils.ApiKey); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/search/person?api_key={1}&query={0}", + WebUtility.UrlEncode(searchInfo.Name), + TmdbUtils.ApiKey); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) @@ -200,8 +206,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return result; } - private readonly CultureInfo _usCulture = new CultureInfo("en-US"); - /// <summary> /// Gets the TMDB id. /// </summary> @@ -226,7 +230,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People return; } - var url = string.Format(TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", TmdbUtils.ApiKey, id); + var url = string.Format( + CultureInfo.InvariantCulture, + TmdbUtils.BaseTmdbApiUrl + @"3/person/{1}?api_key={0}&append_to_response=credits,images,external_ids", + TmdbUtils.ApiKey, + id); using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); foreach (var header in TmdbUtils.AcceptHeaders) diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index f9c6a1674..1ac5f76d6 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index 5c08444df..68381e7bf 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index b54fc3f91..ce1b100c1 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 3a2f67615..b4a3c1b76 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.macos b/deployment/Dockerfile.macos index d1c0784ff..7912e018e 100644 --- a/deployment/Dockerfile.macos +++ b/deployment/Dockerfile.macos @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 7270188fd..949f1ef8f 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index b7d3b4bde..9518d8493 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index dc90f9fbd..0174f2f2a 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index db98610c9..0e02240c8 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 95fd10cf4..d1f2f9e48 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -15,7 +15,7 @@ RUN apt-get update \ # Install dotnet repository # https://dotnet.microsoft.com/download/linux-package-manager/debian9/sdk-current -RUN wget https://download.visualstudio.microsoft.com/download/pr/c1a30ceb-adc2-4244-b24a-06ca29bb1ee9/6df5d856ff1b3e910d283f89690b7cae/dotnet-sdk-3.1.302-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget https://download.visualstudio.microsoft.com/download/pr/4f9b8a64-5e09-456c-a087-527cfc8b4cd2/15e14ec06eab947432de139f172f7a98/dotnet-sdk-3.1.401-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index f77eba376..368b6bf0b 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -14,12 +14,12 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.13.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" /> - <PackageReference Include="AutoFixture.Xunit2" Version="4.12.0" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.6" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" /> + <PackageReference Include="AutoFixture.Xunit2" Version="4.13.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="3.1.7" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> <PackageReference Include="Moq" Version="4.14.5" /> </ItemGroup> diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj index 746474044..e3f87d29b 100644 --- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj +++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj @@ -13,9 +13,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> diff --git a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj index 1559f70ab..5de02a29b 100644 --- a/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj +++ b/tests/Jellyfin.Controller.Tests/Jellyfin.Controller.Tests.csproj @@ -13,9 +13,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> diff --git a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj index e1a089547..3ac60819b 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj +++ b/tests/Jellyfin.MediaEncoding.Tests/Jellyfin.MediaEncoding.Tests.csproj @@ -19,9 +19,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> diff --git a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj index 0e9e91563..37d0a9929 100644 --- a/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj +++ b/tests/Jellyfin.Naming.Tests/Jellyfin.Naming.Tests.csproj @@ -13,9 +13,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> diff --git a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs b/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs deleted file mode 100644 index 39bd94b59..000000000 --- a/tests/Jellyfin.Server.Implementations.Tests/HttpServer/ResponseFilterTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Emby.Server.Implementations.HttpServer; -using Xunit; - -namespace Jellyfin.Server.Implementations.Tests.HttpServer -{ - public class ResponseFilterTests - { - [Theory] - [InlineData(null, null)] - [InlineData("", "")] - [InlineData("This is a clean string.", "This is a clean string.")] - [InlineData("This isn't \n\ra clean string.", "This isn't a clean string.")] - public void RemoveControlCharacters_ValidArgs_Correct(string? input, string? result) - { - Assert.Equal(result, ResponseFilter.RemoveControlCharacters(input)); - } - } -} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj index 03187f4b9..d1679c279 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj +++ b/tests/Jellyfin.Server.Implementations.Tests/Jellyfin.Server.Implementations.Tests.csproj @@ -15,10 +15,11 @@ <ItemGroup> <PackageReference Include="AutoFixture" Version="4.13.0" /> - <PackageReference Include="AutoFixture.AutoMoq" Version="4.12.0" /> + <PackageReference Include="AutoFixture.AutoMoq" Version="4.13.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="Moq" Version="4.14.5" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> diff --git a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj index a4ef10648..b3fd853e2 100644 --- a/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj +++ b/tests/MediaBrowser.Api.Tests/MediaBrowser.Api.Tests.csproj @@ -8,10 +8,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.6" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.7" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> <PackageReference Include="coverlet.collector" Version="1.3.0" /> </ItemGroup> |
