mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Merge remote-tracking branch 'upstream/master' into output-formatters
This commit is contained in:
commit
a523ff840c
@ -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
|
||||
|
@ -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,38 +130,85 @@ 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:
|
||||
- BuildPackage
|
||||
condition: and(succeeded('BuildPackage'), startsWith(variables['Build.SourceBranch'], 'refs/tags'))
|
||||
condition: succeeded('BuildPackage')
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- task: NuGetCommand@2
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Stable Nuget packages'
|
||||
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
|
||||
packDestination: '$(Build.ArtifactStagingDirectory)'
|
||||
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'
|
||||
versioningScheme: 'off'
|
||||
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: 'Build Unstable Nuget packages'
|
||||
inputs:
|
||||
command: 'custom'
|
||||
projects: |
|
||||
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
|
||||
custom: 'pack'
|
||||
arguments: '--version-suffix $(Build.BuildNumber) -o $(Build.ArtifactStagingDirectory) -p:Stability=Unstable'
|
||||
|
||||
- task: PublishBuildArtifacts@1
|
||||
displayName: 'Publish Nuget packages'
|
||||
inputs:
|
||||
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'
|
||||
includeNugetOrg: 'true'
|
||||
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 unstable feed'
|
||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||
inputs:
|
||||
command: 'push'
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -14,7 +14,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Eventing
|
||||
{
|
||||
public class EventManager : IEventManager
|
||||
public class DlnaEventManager : IDlnaEventManager
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, EventSubscription> _subscriptions =
|
||||
new ConcurrentDictionary<string, EventSubscription>(StringComparer.OrdinalIgnoreCase);
|
||||
@ -24,7 +24,7 @@ namespace Emby.Dlna.Eventing
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public EventManager(ILogger logger, IHttpClient httpClient)
|
||||
public DlnaEventManager(ILogger logger, IHttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IConnectionManager : IEventManager, IUpnpService
|
||||
public interface IConnectionManager : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IContentDirectory : IEventManager, IUpnpService
|
||||
public interface IContentDirectory : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IEventManager
|
||||
public interface IDlnaEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Cancels the event subscription.
|
@ -2,7 +2,7 @@
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
|
||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna.Didl;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
@ -18,7 +19,6 @@ using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
@ -6,6 +6,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -6,17 +6,17 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public class BaseService : IEventManager
|
||||
public class BaseService : IDlnaEventManager
|
||||
{
|
||||
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
|
||||
{
|
||||
Logger = logger;
|
||||
HttpClient = httpClient;
|
||||
|
||||
EventManager = new EventManager(logger, HttpClient);
|
||||
EventManager = new DlnaEventManager(logger, HttpClient);
|
||||
}
|
||||
|
||||
protected IEventManager EventManager { get; }
|
||||
protected IDlnaEventManager EventManager { get; }
|
||||
|
||||
protected IHttpClient HttpClient { get; }
|
||||
|
||||
|
@ -3,9 +3,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
||||
|
@ -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>
|
||||
@ -23,10 +32,15 @@
|
||||
<PropertyGroup>
|
||||
<Authors>Jellyfin Contributors</Authors>
|
||||
<PackageId>Jellyfin.Naming</PackageId>
|
||||
<PackageLicenseUrl>https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt</PackageLicenseUrl>
|
||||
<VersionPrefix>10.7.0</VersionPrefix>
|
||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||
<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" /> -->
|
||||
|
@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -13,7 +14,6 @@ using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Notifications;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -1,590 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Notifications;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Model.Updates;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.Activity
|
||||
{
|
||||
/// <summary>
|
||||
/// Entry point for the activity logger.
|
||||
/// </summary>
|
||||
public sealed class ActivityLogEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly ILogger<ActivityLogEntryPoint> _logger;
|
||||
private readonly IInstallationManager _installationManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ITaskManager _taskManager;
|
||||
private readonly IActivityManager _activityManager;
|
||||
private readonly ILocalizationManager _localization;
|
||||
private readonly ISubtitleManager _subManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityLogEntryPoint"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
/// <param name="taskManager">The task manager.</param>
|
||||
/// <param name="activityManager">The activity manager.</param>
|
||||
/// <param name="localization">The localization manager.</param>
|
||||
/// <param name="installationManager">The installation manager.</param>
|
||||
/// <param name="subManager">The subtitle manager.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
public ActivityLogEntryPoint(
|
||||
ILogger<ActivityLogEntryPoint> logger,
|
||||
ISessionManager sessionManager,
|
||||
ITaskManager taskManager,
|
||||
IActivityManager activityManager,
|
||||
ILocalizationManager localization,
|
||||
IInstallationManager installationManager,
|
||||
ISubtitleManager subManager,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_sessionManager = sessionManager;
|
||||
_taskManager = taskManager;
|
||||
_activityManager = activityManager;
|
||||
_localization = localization;
|
||||
_installationManager = installationManager;
|
||||
_subManager = subManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_taskManager.TaskCompleted += OnTaskCompleted;
|
||||
|
||||
_installationManager.PluginInstalled += OnPluginInstalled;
|
||||
_installationManager.PluginUninstalled += OnPluginUninstalled;
|
||||
_installationManager.PluginUpdated += OnPluginUpdated;
|
||||
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
|
||||
|
||||
_sessionManager.SessionStarted += OnSessionStarted;
|
||||
_sessionManager.AuthenticationFailed += OnAuthenticationFailed;
|
||||
_sessionManager.AuthenticationSucceeded += OnAuthenticationSucceeded;
|
||||
_sessionManager.SessionEnded += OnSessionEnded;
|
||||
_sessionManager.PlaybackStart += OnPlaybackStart;
|
||||
_sessionManager.PlaybackStopped += OnPlaybackStopped;
|
||||
|
||||
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
|
||||
|
||||
_userManager.OnUserCreated += OnUserCreated;
|
||||
_userManager.OnUserPasswordChanged += OnUserPasswordChanged;
|
||||
_userManager.OnUserDeleted += OnUserDeleted;
|
||||
_userManager.OnUserLockedOut += OnUserLockedOut;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserLockedOutWithName"),
|
||||
e.Argument.Username),
|
||||
NotificationType.UserLockedOut.ToString(),
|
||||
e.Argument.Id)
|
||||
{
|
||||
LogSeverity = LogLevel.Error
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("SubtitleDownloadFailureFromForItem"),
|
||||
e.Provider,
|
||||
Notifications.NotificationEntryPoint.GetItemName(e.Item)),
|
||||
"SubtitleDownloadFailure",
|
||||
Guid.Empty)
|
||||
{
|
||||
ItemId = e.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
ShortOverview = e.Exception.Message
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPlaybackStopped(object sender, PlaybackStopEventArgs e)
|
||||
{
|
||||
var item = e.MediaInfo;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("PlaybackStopped reported with null media info.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Item != null && e.Item.IsThemeMedia)
|
||||
{
|
||||
// Don't report theme song or local trailer playback
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Users.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = e.Users[0];
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
|
||||
user.Username,
|
||||
GetItemName(item),
|
||||
e.DeviceName),
|
||||
GetPlaybackStoppedNotificationType(item.MediaType),
|
||||
user.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPlaybackStart(object sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
var item = e.MediaInfo;
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
_logger.LogWarning("PlaybackStart reported with null media info.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Item != null && e.Item.IsThemeMedia)
|
||||
{
|
||||
// Don't report theme song or local trailer playback
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Users.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var user = e.Users.First();
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
|
||||
user.Username,
|
||||
GetItemName(item),
|
||||
e.DeviceName),
|
||||
GetPlaybackNotificationType(item.MediaType),
|
||||
user.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GetItemName(BaseItemDto item)
|
||||
{
|
||||
var name = item.Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(item.SeriesName))
|
||||
{
|
||||
name = item.SeriesName + " - " + name;
|
||||
}
|
||||
|
||||
if (item.Artists != null && item.Artists.Count > 0)
|
||||
{
|
||||
name = item.Artists[0] + " - " + name;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string GetPlaybackNotificationType(string mediaType)
|
||||
{
|
||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.AudioPlayback.ToString();
|
||||
}
|
||||
|
||||
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.VideoPlayback.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetPlaybackStoppedNotificationType(string mediaType)
|
||||
{
|
||||
if (string.Equals(mediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.AudioPlaybackStopped.ToString();
|
||||
}
|
||||
|
||||
if (string.Equals(mediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return NotificationType.VideoPlaybackStopped.ToString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async void OnSessionEnded(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
if (string.IsNullOrEmpty(session.UserName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserOfflineFromDevice"),
|
||||
session.UserName,
|
||||
session.DeviceName),
|
||||
"SessionEnded",
|
||||
session.UserId)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
session.RemoteEndPoint),
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnAuthenticationSucceeded(object sender, GenericEventArgs<AuthenticationResult> e)
|
||||
{
|
||||
var user = e.Argument.User;
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
||||
user.Name),
|
||||
"AuthenticationSucceeded",
|
||||
user.Id)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
e.Argument.SessionInfo.RemoteEndPoint),
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnAuthenticationFailed(object sender, GenericEventArgs<AuthenticationRequest> e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
||||
e.Argument.Username),
|
||||
"AuthenticationFailed",
|
||||
Guid.Empty)
|
||||
{
|
||||
LogSeverity = LogLevel.Error,
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
e.Argument.RemoteEndPoint),
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserDeletedWithName"),
|
||||
e.Argument.Username),
|
||||
"UserDeleted",
|
||||
Guid.Empty))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserPasswordChangedWithName"),
|
||||
e.Argument.Username),
|
||||
"UserPasswordChanged",
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserCreatedWithName"),
|
||||
e.Argument.Username),
|
||||
"UserCreated",
|
||||
e.Argument.Id))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnSessionStarted(object sender, SessionEventArgs e)
|
||||
{
|
||||
var session = e.SessionInfo;
|
||||
|
||||
if (string.IsNullOrEmpty(session.UserName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("UserOnlineFromDevice"),
|
||||
session.UserName,
|
||||
session.DeviceName),
|
||||
"SessionStarted",
|
||||
session.UserId)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelIpAddressValue"),
|
||||
session.RemoteEndPoint)
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPluginUpdated(object sender, InstallationInfo e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginUpdatedWithName"),
|
||||
e.Name),
|
||||
NotificationType.PluginUpdateInstalled.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Version),
|
||||
Overview = e.Changelog
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPluginUninstalled(object sender, IPlugin e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginUninstalledWithName"),
|
||||
e.Name),
|
||||
NotificationType.PluginUninstalled.ToString(),
|
||||
Guid.Empty))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPluginInstalled(object sender, InstallationInfo e)
|
||||
{
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("PluginInstalledWithName"),
|
||||
e.Name),
|
||||
NotificationType.PluginInstalled.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
e.Version)
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
|
||||
{
|
||||
var installationInfo = e.InstallationInfo;
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("NameInstallFailed"),
|
||||
installationInfo.Name),
|
||||
NotificationType.InstallationFailed.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
ShortOverview = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("VersionNumber"),
|
||||
installationInfo.Version),
|
||||
Overview = e.Exception.Message
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
|
||||
{
|
||||
var result = e.Result;
|
||||
var task = e.Task;
|
||||
|
||||
if (task.ScheduledTask is IConfigurableScheduledTask activityTask
|
||||
&& !activityTask.IsLogged)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var time = result.EndTimeUtc - result.StartTimeUtc;
|
||||
var runningTime = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localization.GetLocalizedString("LabelRunningTimeValue"),
|
||||
ToUserFriendlyString(time));
|
||||
|
||||
if (result.Status == TaskCompletionStatus.Failed)
|
||||
{
|
||||
var vals = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(e.Result.ErrorMessage))
|
||||
{
|
||||
vals.Add(e.Result.ErrorMessage);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(e.Result.LongErrorMessage))
|
||||
{
|
||||
vals.Add(e.Result.LongErrorMessage);
|
||||
}
|
||||
|
||||
await CreateLogEntry(new ActivityLog(
|
||||
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("ScheduledTaskFailedWithName"), task.Name),
|
||||
NotificationType.TaskFailed.ToString(),
|
||||
Guid.Empty)
|
||||
{
|
||||
LogSeverity = LogLevel.Error,
|
||||
Overview = string.Join(Environment.NewLine, vals),
|
||||
ShortOverview = runningTime
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CreateLogEntry(ActivityLog entry)
|
||||
=> await _activityManager.CreateAsync(entry).ConfigureAwait(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_taskManager.TaskCompleted -= OnTaskCompleted;
|
||||
|
||||
_installationManager.PluginInstalled -= OnPluginInstalled;
|
||||
_installationManager.PluginUninstalled -= OnPluginUninstalled;
|
||||
_installationManager.PluginUpdated -= OnPluginUpdated;
|
||||
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
|
||||
|
||||
_sessionManager.SessionStarted -= OnSessionStarted;
|
||||
_sessionManager.AuthenticationFailed -= OnAuthenticationFailed;
|
||||
_sessionManager.AuthenticationSucceeded -= OnAuthenticationSucceeded;
|
||||
_sessionManager.SessionEnded -= OnSessionEnded;
|
||||
|
||||
_sessionManager.PlaybackStart -= OnPlaybackStart;
|
||||
_sessionManager.PlaybackStopped -= OnPlaybackStopped;
|
||||
|
||||
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
|
||||
|
||||
_userManager.OnUserCreated -= OnUserCreated;
|
||||
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
|
||||
_userManager.OnUserDeleted -= OnUserDeleted;
|
||||
_userManager.OnUserLockedOut -= OnUserLockedOut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a user-friendly string for this TimeSpan instance.
|
||||
/// </summary>
|
||||
private static string ToUserFriendlyString(TimeSpan span)
|
||||
{
|
||||
const int DaysInYear = 365;
|
||||
const int DaysInMonth = 30;
|
||||
|
||||
// Get each non-zero value from TimeSpan component
|
||||
var values = new List<string>();
|
||||
|
||||
// Number of years
|
||||
int days = span.Days;
|
||||
if (days >= DaysInYear)
|
||||
{
|
||||
int years = days / DaysInYear;
|
||||
values.Add(CreateValueString(years, "year"));
|
||||
days %= DaysInYear;
|
||||
}
|
||||
|
||||
// Number of months
|
||||
if (days >= DaysInMonth)
|
||||
{
|
||||
int months = days / DaysInMonth;
|
||||
values.Add(CreateValueString(months, "month"));
|
||||
days = days % DaysInMonth;
|
||||
}
|
||||
|
||||
// Number of days
|
||||
if (days >= 1)
|
||||
{
|
||||
values.Add(CreateValueString(days, "day"));
|
||||
}
|
||||
|
||||
// Number of hours
|
||||
if (span.Hours >= 1)
|
||||
{
|
||||
values.Add(CreateValueString(span.Hours, "hour"));
|
||||
}
|
||||
|
||||
// Number of minutes
|
||||
if (span.Minutes >= 1)
|
||||
{
|
||||
values.Add(CreateValueString(span.Minutes, "minute"));
|
||||
}
|
||||
|
||||
// Number of seconds (include when 0 if no other components included)
|
||||
if (span.Seconds >= 1 || values.Count == 0)
|
||||
{
|
||||
values.Add(CreateValueString(span.Seconds, "second"));
|
||||
}
|
||||
|
||||
// Combine values into string
|
||||
var builder = new StringBuilder();
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
{
|
||||
builder.Append(i == values.Count - 1 ? " and " : ", ");
|
||||
}
|
||||
|
||||
builder.Append(values[i]);
|
||||
}
|
||||
|
||||
// Return result
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a string description of a time-span value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value of this item.</param>
|
||||
/// <param name="description">The name of this item (singular form).</param>
|
||||
private static string CreateValueString(int value, string description)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0:#,##0} {1}",
|
||||
value,
|
||||
value == 1 ? description : string.Format(CultureInfo.InvariantCulture, "{0}s", description));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -53,7 +53,6 @@ using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Chapters;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
@ -72,6 +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;
|
||||
@ -89,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;
|
||||
@ -97,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
|
||||
{
|
||||
@ -122,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
|
||||
@ -173,6 +177,8 @@ namespace Emby.Server.Implementations
|
||||
/// </summary>
|
||||
protected ILogger<ApplicationHost> Logger { get; }
|
||||
|
||||
protected IServiceCollection ServiceCollection { get; }
|
||||
|
||||
private IPlugin[] _plugins;
|
||||
|
||||
/// <summary>
|
||||
@ -238,9 +244,11 @@ namespace Emby.Server.Implementations
|
||||
ILoggerFactory loggerFactory,
|
||||
IStartupOptions options,
|
||||
IFileSystem fileSystem,
|
||||
INetworkManager networkManager)
|
||||
INetworkManager networkManager,
|
||||
IServiceCollection serviceCollection)
|
||||
{
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
ServiceCollection = serviceCollection;
|
||||
|
||||
_networkManager = networkManager;
|
||||
networkManager.LocalSubnetsFn = GetConfiguredLocalSubnets;
|
||||
@ -440,8 +448,7 @@ namespace Emby.Server.Implementations
|
||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
|
||||
Logger.LogInformation("Core startup complete");
|
||||
_httpServer.GlobalResponse = null;
|
||||
|
||||
CoreStartupHasCompleted = true;
|
||||
stopWatch.Restart();
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
@ -464,7 +471,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Init(IServiceCollection serviceCollection)
|
||||
public void Init()
|
||||
{
|
||||
HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber;
|
||||
HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber;
|
||||
@ -493,148 +500,143 @@ namespace Emby.Server.Implementations
|
||||
|
||||
DiscoverTypes();
|
||||
|
||||
RegisterServices(serviceCollection);
|
||||
RegisterServices();
|
||||
}
|
||||
|
||||
public Task ExecuteHttpHandlerAsync(HttpContext context, Func<Task> next)
|
||||
=> _httpServer.RequestHandler(context);
|
||||
|
||||
/// <summary>
|
||||
/// Registers services/resources with the service collection that will be available via DI.
|
||||
/// </summary>
|
||||
protected virtual void RegisterServices(IServiceCollection serviceCollection)
|
||||
protected virtual void RegisterServices()
|
||||
{
|
||||
serviceCollection.AddSingleton(_startupOptions);
|
||||
ServiceCollection.AddSingleton(_startupOptions);
|
||||
|
||||
serviceCollection.AddMemoryCache();
|
||||
ServiceCollection.AddMemoryCache();
|
||||
|
||||
serviceCollection.AddSingleton(ConfigurationManager);
|
||||
serviceCollection.AddSingleton<IApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton(ConfigurationManager);
|
||||
ServiceCollection.AddSingleton<IApplicationHost>(this);
|
||||
|
||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
|
||||
serviceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||
ServiceCollection.AddSingleton<IJsonSerializer, JsonSerializer>();
|
||||
|
||||
serviceCollection.AddSingleton(_fileSystemManager);
|
||||
serviceCollection.AddSingleton<TvdbClientManager>();
|
||||
ServiceCollection.AddSingleton(_fileSystemManager);
|
||||
ServiceCollection.AddSingleton<TvdbClientManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
|
||||
ServiceCollection.AddSingleton<IHttpClient, HttpClientManager.HttpClientManager>();
|
||||
|
||||
serviceCollection.AddSingleton(_networkManager);
|
||||
ServiceCollection.AddSingleton(_networkManager);
|
||||
|
||||
serviceCollection.AddSingleton<IIsoManager, IsoManager>();
|
||||
ServiceCollection.AddSingleton<IIsoManager, IsoManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ITaskManager, TaskManager>();
|
||||
ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
|
||||
|
||||
serviceCollection.AddSingleton(_xmlSerializer);
|
||||
ServiceCollection.AddSingleton(_xmlSerializer);
|
||||
|
||||
serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
|
||||
ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
|
||||
ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
|
||||
|
||||
serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
|
||||
ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
|
||||
|
||||
serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
|
||||
ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IZipClient, ZipClient>();
|
||||
ServiceCollection.AddSingleton<IZipClient, ZipClient>();
|
||||
|
||||
serviceCollection.AddSingleton<IHttpResultFactory, HttpResultFactory>();
|
||||
ServiceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
|
||||
serviceCollection.AddSingleton<IServerApplicationHost>(this);
|
||||
serviceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
|
||||
ServiceCollection.AddSingleton(ServerConfigurationManager);
|
||||
|
||||
serviceCollection.AddSingleton(ServerConfigurationManager);
|
||||
ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||
ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
|
||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
||||
ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||
ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||
ServiceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
||||
ServiceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
||||
serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
ServiceCollection.AddTransient(provider => new Lazy<EncodingHelper>(provider.GetRequiredService<EncodingHelper>));
|
||||
ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
|
||||
ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
|
||||
ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
|
||||
ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||
ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
|
||||
|
||||
serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
|
||||
|
||||
serviceCollection.AddSingleton<ServiceController>();
|
||||
serviceCollection.AddSingleton<IHttpServer, HttpListenerHost>();
|
||||
ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||
ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
|
||||
|
||||
serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||
ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||
ServiceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||
ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||
ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
|
||||
ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
|
||||
|
||||
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
|
||||
serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
|
||||
serviceCollection.AddSingleton<IDtoService, DtoService>();
|
||||
ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
|
||||
ServiceCollection.AddSingleton<IDtoService, DtoService>();
|
||||
|
||||
serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
|
||||
ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
|
||||
ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
|
||||
|
||||
serviceCollection.AddSingleton<LiveTvDtoService>();
|
||||
serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
|
||||
ServiceCollection.AddSingleton<LiveTvDtoService>();
|
||||
ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
|
||||
ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||
serviceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||
ServiceCollection.AddSingleton<IAuthorizationContext, AuthorizationContext>();
|
||||
ServiceCollection.AddSingleton<ISessionContext, SessionContext>();
|
||||
|
||||
serviceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
ServiceCollection.AddSingleton<IAuthService, AuthService>();
|
||||
ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||
ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
|
||||
|
||||
serviceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
|
||||
serviceCollection.AddSingleton<EncodingHelper>();
|
||||
ServiceCollection.AddSingleton<IResourceFileManager, ResourceFileManager>();
|
||||
ServiceCollection.AddSingleton<EncodingHelper>();
|
||||
|
||||
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
|
||||
|
||||
serviceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
serviceCollection.AddScoped<MediaInfoHelper>();
|
||||
serviceCollection.AddScoped<AudioHelper>();
|
||||
serviceCollection.AddScoped<DynamicHlsHelper>();
|
||||
ServiceCollection.AddSingleton<TranscodingJobHelper>();
|
||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||
ServiceCollection.AddScoped<AudioHelper>();
|
||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -648,7 +650,7 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_mediaEncoder = Resolve<IMediaEncoder>();
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
_httpServer = Resolve<IHttpServer>();
|
||||
_webSocketManager = Resolve<IWebSocketManager>();
|
||||
_httpClient = Resolve<IHttpClient>();
|
||||
|
||||
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
|
||||
@ -750,7 +752,6 @@ namespace Emby.Server.Implementations
|
||||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -770,7 +771,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>(),
|
||||
@ -834,6 +836,8 @@ namespace Emby.Server.Implementations
|
||||
{
|
||||
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
|
||||
}
|
||||
|
||||
plugin.RegisterServices(ServiceCollection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -934,7 +938,7 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
|
||||
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
requiresRestart = true;
|
||||
}
|
||||
@ -1388,6 +1392,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)
|
||||
|
@ -2,11 +2,11 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Emby.Server.Implementations.AppBase;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -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 },
|
||||
|
@ -7,13 +7,13 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
@ -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" />
|
||||
|
@ -7,11 +7,11 @@ using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Mono.Nat;
|
||||
|
||||
|
@ -7,6 +7,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
@ -15,7 +16,6 @@ using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
@ -43,22 +44,22 @@ namespace Emby.Server.Implementations.EntryPoints
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnLiveTvManagerSeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
|
||||
private async void OnLiveTvManagerSeriesTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
|
||||
{
|
||||
await SendMessage("SeriesTimerCreated", e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnLiveTvManagerTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
|
||||
private async void OnLiveTvManagerTimerCreated(object sender, GenericEventArgs<TimerEventInfo> e)
|
||||
{
|
||||
await SendMessage("TimerCreated", e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
|
||||
private async void OnLiveTvManagerSeriesTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
|
||||
{
|
||||
await SendMessage("SeriesTimerCancelled", e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnLiveTvManagerTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs<TimerEventInfo> e)
|
||||
private async void OnLiveTvManagerTimerCancelled(object sender, GenericEventArgs<TimerEventInfo> e)
|
||||
{
|
||||
await SendMessage("TimerCancelled", e.Argument).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -1,210 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Plugins;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using MediaBrowser.Model.Updates;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Class WebSocketEvents.
|
||||
/// </summary>
|
||||
public class ServerEventNotifier : IServerEntryPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The user manager.
|
||||
/// </summary>
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// The installation manager.
|
||||
/// </summary>
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
||||
/// <summary>
|
||||
/// The kernel.
|
||||
/// </summary>
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
/// <summary>
|
||||
/// The task manager.
|
||||
/// </summary>
|
||||
private readonly ITaskManager _taskManager;
|
||||
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ServerEventNotifier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="userManager">The user manager.</param>
|
||||
/// <param name="installationManager">The installation manager.</param>
|
||||
/// <param name="taskManager">The task manager.</param>
|
||||
/// <param name="sessionManager">The session manager.</param>
|
||||
public ServerEventNotifier(
|
||||
IServerApplicationHost appHost,
|
||||
IUserManager userManager,
|
||||
IInstallationManager installationManager,
|
||||
ITaskManager taskManager,
|
||||
ISessionManager sessionManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_installationManager = installationManager;
|
||||
_appHost = appHost;
|
||||
_taskManager = taskManager;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_userManager.OnUserDeleted += OnUserDeleted;
|
||||
_userManager.OnUserUpdated += OnUserUpdated;
|
||||
|
||||
_appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
|
||||
|
||||
_installationManager.PluginUninstalled += OnPluginUninstalled;
|
||||
_installationManager.PackageInstalling += OnPackageInstalling;
|
||||
_installationManager.PackageInstallationCancelled += OnPackageInstallationCancelled;
|
||||
_installationManager.PackageInstallationCompleted += OnPackageInstallationCompleted;
|
||||
_installationManager.PackageInstallationFailed += OnPackageInstallationFailed;
|
||||
|
||||
_taskManager.TaskCompleted += OnTaskCompleted;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnPackageInstalling(object sender, InstallationInfo e)
|
||||
{
|
||||
await SendMessageToAdminSessions("PackageInstalling", e).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPackageInstallationCancelled(object sender, InstallationInfo e)
|
||||
{
|
||||
await SendMessageToAdminSessions("PackageInstallationCancelled", e).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPackageInstallationCompleted(object sender, InstallationInfo e)
|
||||
{
|
||||
await SendMessageToAdminSessions("PackageInstallationCompleted", e).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnPackageInstallationFailed(object sender, InstallationFailedEventArgs e)
|
||||
{
|
||||
await SendMessageToAdminSessions("PackageInstallationFailed", e.InstallationInfo).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnTaskCompleted(object sender, TaskCompletionEventArgs e)
|
||||
{
|
||||
await SendMessageToAdminSessions("ScheduledTaskEnded", e.Result).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installations the manager_ plugin uninstalled.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The e.</param>
|
||||
private async void OnPluginUninstalled(object sender, IPlugin e)
|
||||
{
|
||||
await SendMessageToAdminSessions("PluginUninstalled", e).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the HasPendingRestartChanged event of the kernel control.
|
||||
/// </summary>
|
||||
/// <param name="sender">The source of the event.</param>
|
||||
/// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
|
||||
private async void OnHasPendingRestartChanged(object sender, EventArgs e)
|
||||
{
|
||||
await _sessionManager.SendRestartRequiredNotification(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Users the manager_ user updated.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The e.</param>
|
||||
private async void OnUserUpdated(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
var dto = _userManager.GetUserDto(e.Argument);
|
||||
|
||||
await SendMessageToUserSession(e.Argument, "UserUpdated", dto).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Users the manager_ user deleted.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender.</param>
|
||||
/// <param name="e">The e.</param>
|
||||
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
|
||||
{
|
||||
await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendMessageToAdminSessions<T>(string name, T data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendMessageToAdminSessions(name, data, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendMessageToUserSession<T>(User user, string name, T data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendMessageToUserSessions(
|
||||
new List<Guid> { user.Id },
|
||||
name,
|
||||
data,
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool dispose)
|
||||
{
|
||||
if (dispose)
|
||||
{
|
||||
_userManager.OnUserDeleted -= OnUserDeleted;
|
||||
_userManager.OnUserUpdated -= OnUserUpdated;
|
||||
|
||||
_installationManager.PluginUninstalled -= OnPluginUninstalled;
|
||||
_installationManager.PackageInstalling -= OnPackageInstalling;
|
||||
_installationManager.PackageInstallationCancelled -= OnPackageInstallationCancelled;
|
||||
_installationManager.PackageInstallationCompleted -= OnPackageInstallationCompleted;
|
||||
_installationManager.PackageInstallationFailed -= OnPackageInstallationFailed;
|
||||
|
||||
_appHost.HasPendingRestartChanged -= OnHasPendingRestartChanged;
|
||||
|
||||
_taskManager.TaskCompleted -= OnTaskCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,766 +0,0 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Sockets;
|
||||
using System.Net.WebSockets;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Services;
|
||||
using Emby.Server.Implementations.SocketSharp;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using ServiceStack.Text.Jsv;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
{
|
||||
public class HttpListenerHost : IHttpServer
|
||||
{
|
||||
/// <summary>
|
||||
/// The key for a setting that specifies the default redirect path
|
||||
/// to use for requests where the URL base prefix is invalid or missing.
|
||||
/// </summary>
|
||||
public const string DefaultRedirectKey = "HttpListenerHost:DefaultRedirectPath";
|
||||
|
||||
private readonly ILogger<HttpListenerHost> _logger;
|
||||
private readonly ILoggerFactory _loggerFactory;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly Func<Type, Func<string, object>> _funcParseFn;
|
||||
private readonly string _defaultRedirectPath;
|
||||
private readonly string _baseUrlPrefix;
|
||||
|
||||
private readonly Dictionary<Type, Type> _serviceOperationsMap = new Dictionary<Type, Type>();
|
||||
private readonly IHostEnvironment _hostEnvironment;
|
||||
|
||||
private IWebSocketListener[] _webSocketListeners = Array.Empty<IWebSocketListener>();
|
||||
private bool _disposed = false;
|
||||
|
||||
public HttpListenerHost(
|
||||
IServerApplicationHost applicationHost,
|
||||
ILogger<HttpListenerHost> logger,
|
||||
IServerConfigurationManager config,
|
||||
IConfiguration configuration,
|
||||
INetworkManager networkManager,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IXmlSerializer xmlSerializer,
|
||||
ILocalizationManager localizationManager,
|
||||
ServiceController serviceController,
|
||||
IHostEnvironment hostEnvironment,
|
||||
ILoggerFactory loggerFactory)
|
||||
{
|
||||
_appHost = applicationHost;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
_defaultRedirectPath = configuration[DefaultRedirectKey];
|
||||
_baseUrlPrefix = _config.Configuration.BaseUrl;
|
||||
_networkManager = networkManager;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_xmlSerializer = xmlSerializer;
|
||||
ServiceController = serviceController;
|
||||
_hostEnvironment = hostEnvironment;
|
||||
_loggerFactory = loggerFactory;
|
||||
|
||||
_funcParseFn = t => s => JsvReader.GetParseFn(t)(s);
|
||||
|
||||
Instance = this;
|
||||
ResponseFilters = Array.Empty<Action<IRequest, HttpResponse, object>>();
|
||||
GlobalResponse = localizationManager.GetLocalizedString("StartupEmbyServerIsLoading");
|
||||
}
|
||||
|
||||
public event EventHandler<GenericEventArgs<IWebSocketConnection>> WebSocketConnected;
|
||||
|
||||
public Action<IRequest, HttpResponse, object>[] ResponseFilters { get; set; }
|
||||
|
||||
public static HttpListenerHost Instance { get; protected set; }
|
||||
|
||||
public string[] UrlPrefixes { get; private set; }
|
||||
|
||||
public string GlobalResponse { get; set; }
|
||||
|
||||
public ServiceController ServiceController { get; }
|
||||
|
||||
public object CreateInstance(Type type)
|
||||
{
|
||||
return _appHost.CreateInstance(type);
|
||||
}
|
||||
|
||||
private static string NormalizeUrlPath(string path)
|
||||
{
|
||||
if (path.Length > 0 && path[0] == '/')
|
||||
{
|
||||
// If the path begins with a leading slash, just return it as-is
|
||||
return path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the path does not begin with a leading slash, append one for consistency
|
||||
return "/" + path;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the request filters. Returns whether or not the request has been handled
|
||||
/// and no more processing should be done.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public void ApplyRequestFilters(IRequest req, HttpResponse res, object requestDto)
|
||||
{
|
||||
// Exec all RequestFilter attributes with Priority < 0
|
||||
var attributes = GetRequestFilterAttributes(requestDto.GetType());
|
||||
|
||||
int count = attributes.Count;
|
||||
int i = 0;
|
||||
for (; i < count && attributes[i].Priority < 0; i++)
|
||||
{
|
||||
var attribute = attributes[i];
|
||||
attribute.RequestFilter(req, res, requestDto);
|
||||
}
|
||||
|
||||
// Exec remaining RequestFilter attributes with Priority >= 0
|
||||
for (; i < count && attributes[i].Priority >= 0; i++)
|
||||
{
|
||||
var attribute = attributes[i];
|
||||
attribute.RequestFilter(req, res, requestDto);
|
||||
}
|
||||
}
|
||||
|
||||
public Type GetServiceTypeByRequest(Type requestType)
|
||||
{
|
||||
_serviceOperationsMap.TryGetValue(requestType, out var serviceType);
|
||||
return serviceType;
|
||||
}
|
||||
|
||||
public void AddServiceInfo(Type serviceType, Type requestType)
|
||||
{
|
||||
_serviceOperationsMap[requestType] = serviceType;
|
||||
}
|
||||
|
||||
private List<IHasRequestFilter> GetRequestFilterAttributes(Type requestDtoType)
|
||||
{
|
||||
var attributes = requestDtoType.GetCustomAttributes(true).OfType<IHasRequestFilter>().ToList();
|
||||
|
||||
var serviceType = GetServiceTypeByRequest(requestDtoType);
|
||||
if (serviceType != null)
|
||||
{
|
||||
attributes.AddRange(serviceType.GetCustomAttributes(true).OfType<IHasRequestFilter>());
|
||||
}
|
||||
|
||||
attributes.Sort((x, y) => x.Priority - y.Priority);
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static Exception GetActualException(Exception ex)
|
||||
{
|
||||
if (ex is AggregateException agg)
|
||||
{
|
||||
var inner = agg.InnerException;
|
||||
if (inner != null)
|
||||
{
|
||||
return GetActualException(inner);
|
||||
}
|
||||
else
|
||||
{
|
||||
var inners = agg.InnerExceptions;
|
||||
if (inners.Count > 0)
|
||||
{
|
||||
return GetActualException(inners[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ex;
|
||||
}
|
||||
|
||||
private int GetStatusCode(Exception ex)
|
||||
{
|
||||
switch (ex)
|
||||
{
|
||||
case ArgumentException _: return 400;
|
||||
case AuthenticationException _: return 401;
|
||||
case SecurityException _: return 403;
|
||||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return 404;
|
||||
case MethodNotAllowedException _: return 405;
|
||||
default: return 500;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ErrorHandler(Exception ex, IRequest httpReq, int statusCode, string urlToLog, bool ignoreStackTrace)
|
||||
{
|
||||
if (ignoreStackTrace)
|
||||
{
|
||||
_logger.LogError("Error processing request: {Message}. URL: {Url}", ex.Message.TrimEnd('.'), urlToLog);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(ex, "Error processing request. URL: {Url}", urlToLog);
|
||||
}
|
||||
|
||||
var httpRes = httpReq.Response;
|
||||
|
||||
if (httpRes.HasStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
httpRes.StatusCode = statusCode;
|
||||
|
||||
var errContent = _hostEnvironment.IsDevelopment()
|
||||
? (NormalizeExceptionMessage(ex) ?? string.Empty)
|
||||
: "Error processing request.";
|
||||
httpRes.ContentType = "text/plain";
|
||||
httpRes.ContentLength = errContent.Length;
|
||||
await httpRes.WriteAsync(errContent).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string NormalizeExceptionMessage(Exception ex)
|
||||
{
|
||||
// Do not expose the exception message for AuthenticationException
|
||||
if (ex is AuthenticationException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Strip any information we don't want to reveal
|
||||
return ex.Message
|
||||
?.Replace(_config.ApplicationPaths.ProgramSystemPath, string.Empty, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace(_config.ApplicationPaths.ProgramDataPath, string.Empty, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public static string RemoveQueryStringByKey(string url, string key)
|
||||
{
|
||||
var uri = new Uri(url);
|
||||
|
||||
// this gets all the query string key value pairs as a collection
|
||||
var newQueryString = QueryHelpers.ParseQuery(uri.Query);
|
||||
|
||||
var originalCount = newQueryString.Count;
|
||||
|
||||
if (originalCount == 0)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
// this removes the key if exists
|
||||
newQueryString.Remove(key);
|
||||
|
||||
if (originalCount == newQueryString.Count)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
|
||||
// this gets the page path from root without QueryString
|
||||
string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
|
||||
return newQueryString.Count > 0
|
||||
? QueryHelpers.AddQueryString(pagePathWithoutQueryString, newQueryString.ToDictionary(kv => kv.Key, kv => kv.Value.ToString()))
|
||||
: pagePathWithoutQueryString;
|
||||
}
|
||||
|
||||
private static string GetUrlToLog(string url)
|
||||
{
|
||||
url = RemoveQueryStringByKey(url, "api_key");
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
private static string NormalizeConfiguredLocalAddress(string address)
|
||||
{
|
||||
var add = address.AsSpan().Trim('/');
|
||||
int index = add.IndexOf('/');
|
||||
if (index != -1)
|
||||
{
|
||||
add = add.Slice(index + 1);
|
||||
}
|
||||
|
||||
return add.TrimStart('/').ToString();
|
||||
}
|
||||
|
||||
private bool ValidateHost(string host)
|
||||
{
|
||||
var hosts = _config
|
||||
.Configuration
|
||||
.LocalNetworkAddresses
|
||||
.Select(NormalizeConfiguredLocalAddress)
|
||||
.ToList();
|
||||
|
||||
if (hosts.Count == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
host ??= string.Empty;
|
||||
|
||||
if (_networkManager.IsInPrivateAddressSpace(host))
|
||||
{
|
||||
hosts.Add("localhost");
|
||||
hosts.Add("127.0.0.1");
|
||||
|
||||
return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ValidateRequest(string remoteIp, bool isLocal)
|
||||
{
|
||||
if (isLocal)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_config.Configuration.EnableRemoteAccess)
|
||||
{
|
||||
var addressFilter = _config.Configuration.RemoteIPFilter.Where(i => !string.IsNullOrWhiteSpace(i)).ToArray();
|
||||
|
||||
if (addressFilter.Length > 0 && !_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
if (_config.Configuration.IsRemoteIPFilterBlacklist)
|
||||
{
|
||||
return !_networkManager.IsAddressInSubnets(remoteIp, addressFilter);
|
||||
}
|
||||
else
|
||||
{
|
||||
return _networkManager.IsAddressInSubnets(remoteIp, addressFilter);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate a connection from a remote IP address to a URL to see if a redirection to HTTPS is required.
|
||||
/// </summary>
|
||||
/// <returns>True if the request is valid, or false if the request is not valid and an HTTPS redirect is required.</returns>
|
||||
private bool ValidateSsl(string remoteIp, string urlString)
|
||||
{
|
||||
if (_config.Configuration.RequireHttps
|
||||
&& _appHost.ListenWithHttps
|
||||
&& !urlString.Contains("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// These are hacks, but if these ever occur on ipv6 in the local network they could be incorrectly redirected
|
||||
if (urlString.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) != -1
|
||||
|| urlString.IndexOf("dlna/", StringComparison.OrdinalIgnoreCase) != -1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_networkManager.IsInLocalNetwork(remoteIp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RequestHandler(HttpContext context)
|
||||
{
|
||||
if (context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
return WebSocketRequestHandler(context);
|
||||
}
|
||||
|
||||
var request = context.Request;
|
||||
var response = context.Response;
|
||||
var localPath = context.Request.Path.ToString();
|
||||
|
||||
var req = new WebSocketSharpRequest(request, response, request.Path);
|
||||
return RequestHandler(req, request.GetDisplayUrl(), request.Host.ToString(), localPath, context.RequestAborted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridable method that can be used to implement a custom handler.
|
||||
/// </summary>
|
||||
private async Task RequestHandler(IHttpRequest httpReq, string urlString, string host, string localPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
var httpRes = httpReq.Response;
|
||||
string urlToLog = GetUrlToLog(urlString);
|
||||
string remoteIp = httpReq.RemoteIp;
|
||||
|
||||
try
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
httpRes.ContentType = "text/plain";
|
||||
await httpRes.WriteAsync("Server shutting down", cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateHost(host))
|
||||
{
|
||||
httpRes.StatusCode = 400;
|
||||
httpRes.ContentType = "text/plain";
|
||||
await httpRes.WriteAsync("Invalid host", cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateRequest(remoteIp, httpReq.IsLocal))
|
||||
{
|
||||
httpRes.StatusCode = 403;
|
||||
httpRes.ContentType = "text/plain";
|
||||
await httpRes.WriteAsync("Forbidden", cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ValidateSsl(httpReq.RemoteIp, urlString))
|
||||
{
|
||||
RedirectToSecureUrl(httpReq, httpRes, urlString);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
httpRes.StatusCode = 200;
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
|
||||
httpRes.ContentType = "text/plain";
|
||||
await httpRes.WriteAsync(string.Empty, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(localPath, _baseUrlPrefix + "/", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(localPath, _baseUrlPrefix, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.IsNullOrEmpty(localPath)
|
||||
|| !localPath.StartsWith(_baseUrlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Always redirect back to the default path if the base prefix is invalid or missing
|
||||
_logger.LogDebug("Normalizing a URL at {0}", localPath);
|
||||
httpRes.Redirect(_baseUrlPrefix + "/" + _defaultRedirectPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(GlobalResponse))
|
||||
{
|
||||
// We don't want the address pings in ApplicationHost to fail
|
||||
if (localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
httpRes.StatusCode = 503;
|
||||
httpRes.ContentType = "text/html";
|
||||
await httpRes.WriteAsync(GlobalResponse, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var handler = GetServiceHandler(httpReq);
|
||||
if (handler != null)
|
||||
{
|
||||
await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
}
|
||||
catch (Exception requestEx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var requestInnerEx = GetActualException(requestEx);
|
||||
var statusCode = GetStatusCode(requestInnerEx);
|
||||
|
||||
foreach (var (key, value) in GetDefaultCorsHeaders(httpReq))
|
||||
{
|
||||
if (!httpRes.Headers.ContainsKey(key))
|
||||
{
|
||||
httpRes.Headers.Add(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
bool ignoreStackTrace =
|
||||
requestInnerEx is SocketException
|
||||
|| requestInnerEx is IOException
|
||||
|| requestInnerEx is OperationCanceledException
|
||||
|| requestInnerEx is SecurityException
|
||||
|| requestInnerEx is AuthenticationException
|
||||
|| requestInnerEx is FileNotFoundException;
|
||||
|
||||
// Do not handle 500 server exceptions manually when in development mode.
|
||||
// Instead, re-throw the exception so it can be handled by the DeveloperExceptionPageMiddleware.
|
||||
// However, do not use the DeveloperExceptionPageMiddleware when the stack trace should be ignored,
|
||||
// because it will log the stack trace when it handles the exception.
|
||||
if (statusCode == 500 && !ignoreStackTrace && _hostEnvironment.IsDevelopment())
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
await ErrorHandler(requestInnerEx, httpReq, statusCode, urlToLog, ignoreStackTrace).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception handlerException)
|
||||
{
|
||||
var aggregateEx = new AggregateException("Error while handling request exception", requestEx, handlerException);
|
||||
_logger.LogError(aggregateEx, "Error while handling exception in response to {Url}", urlToLog);
|
||||
|
||||
if (_hostEnvironment.IsDevelopment())
|
||||
{
|
||||
throw aggregateEx;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (httpRes.StatusCode >= 500)
|
||||
{
|
||||
_logger.LogDebug("Sending HTTP Response 500 in response to {Url}", urlToLog);
|
||||
}
|
||||
|
||||
stopWatch.Stop();
|
||||
var elapsed = stopWatch.Elapsed;
|
||||
if (elapsed.TotalMilliseconds > 500)
|
||||
{
|
||||
_logger.LogWarning("HTTP Response {StatusCode} to {RemoteIp}. Time (slow): {Elapsed:g}. {Url}", httpRes.StatusCode, remoteIp, elapsed, urlToLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WebSocketRequestHandler(HttpContext context)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||
|
||||
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync().ConfigureAwait(false);
|
||||
|
||||
using var connection = new WebSocketConnection(
|
||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||
webSocket,
|
||||
context.Connection.RemoteIpAddress,
|
||||
context.Request.Query)
|
||||
{
|
||||
OnReceive = ProcessWebSocketMessageReceived
|
||||
};
|
||||
|
||||
WebSocketConnected?.Invoke(this, new GenericEventArgs<IWebSocketConnection>(connection));
|
||||
|
||||
await connection.ProcessAsync().ConfigureAwait(false);
|
||||
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
||||
}
|
||||
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
|
||||
{
|
||||
_logger.LogError(ex, "WS {IP} WebSocketRequestHandler error", context.Connection.RemoteIpAddress);
|
||||
if (!context.Response.HasStarted)
|
||||
{
|
||||
context.Response.StatusCode = 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the default CORS headers.
|
||||
/// </summary>
|
||||
/// <param name="req"></param>
|
||||
/// <returns></returns>
|
||||
public IDictionary<string, string> GetDefaultCorsHeaders(IRequest req)
|
||||
{
|
||||
var origin = req.Headers["Origin"];
|
||||
if (origin == StringValues.Empty)
|
||||
{
|
||||
origin = req.Headers["Host"];
|
||||
if (origin == StringValues.Empty)
|
||||
{
|
||||
origin = "*";
|
||||
}
|
||||
}
|
||||
|
||||
var headers = new Dictionary<string, string>();
|
||||
headers.Add("Access-Control-Allow-Origin", origin);
|
||||
headers.Add("Access-Control-Allow-Credentials", "true");
|
||||
headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
||||
headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization, Cookie");
|
||||
return headers;
|
||||
}
|
||||
|
||||
// Entry point for HttpListener
|
||||
public ServiceHandler GetServiceHandler(IHttpRequest httpReq)
|
||||
{
|
||||
var pathInfo = httpReq.PathInfo;
|
||||
|
||||
pathInfo = ServiceHandler.GetSanitizedPathInfo(pathInfo, out string contentType);
|
||||
var restPath = ServiceController.GetRestPathForRequest(httpReq.HttpMethod, pathInfo);
|
||||
if (restPath != null)
|
||||
{
|
||||
return new ServiceHandler(restPath, contentType);
|
||||
}
|
||||
|
||||
_logger.LogError("Could not find handler for {PathInfo}", pathInfo);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void RedirectToSecureUrl(IHttpRequest httpReq, HttpResponse httpRes, string url)
|
||||
{
|
||||
if (Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
|
||||
{
|
||||
var builder = new UriBuilder(uri)
|
||||
{
|
||||
Port = _config.Configuration.PublicHttpsPort,
|
||||
Scheme = "https"
|
||||
};
|
||||
url = builder.Uri.ToString();
|
||||
}
|
||||
|
||||
httpRes.Redirect(url);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the rest handlers.
|
||||
/// </summary>
|
||||
/// <param name="serviceTypes">The service types to register with the <see cref="ServiceController"/>.</param>
|
||||
/// <param name="listeners">The web socket listeners.</param>
|
||||
/// <param name="urlPrefixes">The URL prefixes. See <see cref="UrlPrefixes"/>.</param>
|
||||
public void Init(IEnumerable<Type> serviceTypes, IEnumerable<IWebSocketListener> listeners, IEnumerable<string> urlPrefixes)
|
||||
{
|
||||
_webSocketListeners = listeners.ToArray();
|
||||
UrlPrefixes = urlPrefixes.ToArray();
|
||||
|
||||
ServiceController.Init(this, serviceTypes);
|
||||
|
||||
ResponseFilters = new Action<IRequest, HttpResponse, object>[]
|
||||
{
|
||||
new ResponseFilter(this, _logger).FilterResponse
|
||||
};
|
||||
}
|
||||
|
||||
public RouteAttribute[] GetRouteAttributes(Type requestType)
|
||||
{
|
||||
var routes = requestType.GetTypeInfo().GetCustomAttributes<RouteAttribute>(true).ToList();
|
||||
var clone = routes.ToList();
|
||||
|
||||
foreach (var route in clone)
|
||||
{
|
||||
routes.Add(new RouteAttribute(NormalizeCustomRoutePath(route.Path), route.Verbs)
|
||||
{
|
||||
Notes = route.Notes,
|
||||
Priority = route.Priority,
|
||||
Summary = route.Summary
|
||||
});
|
||||
|
||||
routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs)
|
||||
{
|
||||
Notes = route.Notes,
|
||||
Priority = route.Priority,
|
||||
Summary = route.Summary
|
||||
});
|
||||
|
||||
routes.Add(new RouteAttribute(NormalizeMediaBrowserRoutePath(route.Path), route.Verbs)
|
||||
{
|
||||
Notes = route.Notes,
|
||||
Priority = route.Priority,
|
||||
Summary = route.Summary
|
||||
});
|
||||
}
|
||||
|
||||
return routes.ToArray();
|
||||
}
|
||||
|
||||
public Func<string, object> GetParseFn(Type propertyType)
|
||||
{
|
||||
return _funcParseFn(propertyType);
|
||||
}
|
||||
|
||||
public void SerializeToJson(object o, Stream stream)
|
||||
{
|
||||
_jsonSerializer.SerializeToStream(o, stream);
|
||||
}
|
||||
|
||||
public void SerializeToXml(object o, Stream stream)
|
||||
{
|
||||
_xmlSerializer.SerializeToStream(o, stream);
|
||||
}
|
||||
|
||||
public Task<object> DeserializeXml(Type type, Stream stream)
|
||||
{
|
||||
return Task.FromResult(_xmlSerializer.DeserializeFromStream(type, stream));
|
||||
}
|
||||
|
||||
public Task<object> DeserializeJson(Type type, Stream stream)
|
||||
{
|
||||
return _jsonSerializer.DeserializeFromStreamAsync(stream, type);
|
||||
}
|
||||
|
||||
private string NormalizeEmbyRoutePath(string path)
|
||||
{
|
||||
_logger.LogDebug("Normalizing /emby route");
|
||||
return _baseUrlPrefix + "/emby" + NormalizeUrlPath(path);
|
||||
}
|
||||
|
||||
private string NormalizeMediaBrowserRoutePath(string path)
|
||||
{
|
||||
_logger.LogDebug("Normalizing /mediabrowser route");
|
||||
return _baseUrlPrefix + "/mediabrowser" + NormalizeUrlPath(path);
|
||||
}
|
||||
|
||||
private string NormalizeCustomRoutePath(string path)
|
||||
{
|
||||
_logger.LogDebug("Normalizing custom route {0}", path);
|
||||
return _baseUrlPrefix + NormalizeUrlPath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes the web socket message received.
|
||||
/// </summary>
|
||||
/// <param name="result">The result.</param>
|
||||
private Task ProcessWebSocketMessageReceived(WebSocketMessageInfo result)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
IEnumerable<Task> GetTasks()
|
||||
{
|
||||
foreach (var x in _webSocketListeners)
|
||||
{
|
||||
yield return x.ProcessMessageAsync(result);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.WhenAll(GetTasks());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
WebSocketMessage<object> stub;
|
||||
WebSocketMessage<object>? stub;
|
||||
try
|
||||
{
|
||||
|
||||
@ -209,6 +209,12 @@ namespace Emby.Server.Implementations.HttpServer
|
||||
return;
|
||||
}
|
||||
|
||||
if (stub == null)
|
||||
{
|
||||
_logger.LogError("Error processing web socket message");
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell the PipeReader how much of the buffer we have consumed
|
||||
reader.AdvanceTo(buffer.End);
|
||||
|
||||
|
102
Emby.Server.Implementations/HttpServer/WebSocketManager.cs
Normal file
102
Emby.Server.Implementations/HttpServer/WebSocketManager.cs
Normal file
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
@ -29,7 +30,6 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
@ -5,8 +5,8 @@ using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -10,6 +10,7 @@ using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
@ -24,7 +25,6 @@ using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Sorting;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
"HeaderContinueWatching": "Lanjutkan Menonton",
|
||||
"HeaderCameraUploads": "Unggahan Kamera",
|
||||
"HeaderAlbumArtists": "Album Artis",
|
||||
"Genres": "Genre",
|
||||
"Genres": "Aliran",
|
||||
"Folders": "Folder",
|
||||
"Favorites": "Favorit",
|
||||
"Collections": "Koleksi",
|
||||
|
@ -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",
|
||||
|
@ -1,73 +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} ล้มเหลว"
|
||||
"ScheduledTaskFailedWithName": "{0} ล้มเหลว",
|
||||
"Songs": "เพลง",
|
||||
"Shows": "รายการ",
|
||||
"ServerNameNeedsToBeRestarted": "{0} ต้องการการรีสตาร์ท",
|
||||
"TaskDownloadMissingSubtitlesDescription": "ค้นหาคำบรรยายที่หายไปในอินเทอร์เน็ตตามค่ากำหนดในข้อมูลเมตา",
|
||||
"TaskDownloadMissingSubtitles": "ดาวน์โหลดคำบรรยายที่ขาดหายไป",
|
||||
"TaskRefreshChannelsDescription": "รีเฟรชข้อมูลช่องอินเทอร์เน็ต",
|
||||
"TaskRefreshChannels": "รีเฟรชช่อง",
|
||||
"TaskCleanTranscodeDescription": "ลบไฟล์ทรานส์โค้ดที่มีอายุมากกว่าหนึ่งวัน",
|
||||
"TaskCleanTranscode": "ล้างไดเรกทอรีทรานส์โค้ด",
|
||||
"TaskUpdatePluginsDescription": "ดาวน์โหลดและติดตั้งโปรแกรมปรับปรุงให้กับปลั๊กอินที่กำหนดค่าให้อัปเดตโดยอัตโนมัติ",
|
||||
"TaskUpdatePlugins": "อัปเดตปลั๊กอิน",
|
||||
"TaskRefreshPeopleDescription": "อัปเดตข้อมูลเมตานักแสดงและผู้กำกับในไลบรารีสื่อ",
|
||||
"TaskRefreshPeople": "รีเฟรชบุคคล",
|
||||
"TaskCleanLogsDescription": "ลบไฟล์บันทึกที่เก่ากว่า {0} วัน",
|
||||
"TaskCleanLogs": "ล้างไดเรกทอรีบันทึก",
|
||||
"TaskRefreshLibraryDescription": "สแกนไลบรารีสื่อของคุณเพื่อหาไฟล์ใหม่และรีเฟรชข้อมูลเมตา",
|
||||
"TaskRefreshLibrary": "สแกนไลบรารีสื่อ",
|
||||
"TaskRefreshChapterImagesDescription": "สร้างภาพขนาดย่อสำหรับวิดีโอที่มีบท",
|
||||
"TaskRefreshChapterImages": "แตกรูปภาพบท",
|
||||
"TaskCleanCacheDescription": "ลบไฟล์แคชที่ระบบไม่ต้องการ",
|
||||
"TaskCleanCache": "ล้างไดเรกทอรีแคช",
|
||||
"TasksChannelsCategory": "ช่องอินเทอร์เน็ต",
|
||||
"TasksApplicationCategory": "แอพพลิเคชัน",
|
||||
"TasksLibraryCategory": "ไลบรารี",
|
||||
"TasksMaintenanceCategory": "ปิดซ่อมบำรุง",
|
||||
"VersionNumber": "เวอร์ชัน {0}",
|
||||
"ValueSpecialEpisodeName": "พิเศษ - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "เพิ่ม {0} ลงในไลบรารีสื่อของคุณแล้ว",
|
||||
"UserStoppedPlayingItemWithValues": "{0} เล่นเสร็จแล้ว {1} บน {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} กำลังเล่น {1} บน {2}",
|
||||
"UserPolicyUpdatedWithName": "มีการอัปเดตนโยบายผู้ใช้ของ {0}",
|
||||
"UserPasswordChangedWithName": "มีการเปลี่ยนรหัสผ่านของผู้ใช้ {0}",
|
||||
"UserOnlineFromDevice": "{0} ออนไลน์จาก {1}",
|
||||
"UserOfflineFromDevice": "{0} ได้ยกเลิกการเชื่อมต่อจาก {1}",
|
||||
"UserLockedOutWithName": "ผู้ใช้ {0} ถูกล็อก",
|
||||
"UserDownloadingItemWithValues": "{0} กำลังดาวน์โหลด {1}",
|
||||
"UserDeletedWithName": "ลบผู้ใช้ {0} แล้ว",
|
||||
"UserCreatedWithName": "สร้างผู้ใช้ {0} แล้ว",
|
||||
"User": "ผู้ใช้งาน",
|
||||
"TvShows": "รายการทีวี",
|
||||
"System": "ระบบ",
|
||||
"Sync": "ซิงค์",
|
||||
"SubtitleDownloadFailureFromForItem": "ไม่สามารถดาวน์โหลดคำบรรยายจาก {0} สำหรับ {1} ได้",
|
||||
"StartupEmbyServerIsLoading": "กำลังโหลดเซิร์ฟเวอร์ Jellyfin โปรดลองอีกครั้งในอีกสักครู่"
|
||||
}
|
||||
|
285
Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
Normal file
285
Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,10 +6,10 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -5,8 +5,8 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller;
|
||||
@ -17,6 +18,8 @@ using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Events.Session;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
@ -24,7 +27,6 @@ using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Session;
|
||||
@ -40,25 +42,16 @@ namespace Emby.Server.Implementations.Session
|
||||
/// </summary>
|
||||
public class SessionManager : ISessionManager, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The user data repository.
|
||||
/// </summary>
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
|
||||
/// <summary>
|
||||
/// The logger.
|
||||
/// </summary>
|
||||
private readonly ILogger<SessionManager> _logger;
|
||||
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IMusicManager _musicManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
|
||||
private readonly IAuthenticationRepository _authRepo;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
||||
@ -75,6 +68,7 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
public SessionManager(
|
||||
ILogger<SessionManager> logger,
|
||||
IEventManager eventManager,
|
||||
IUserDataManager userDataManager,
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
@ -87,6 +81,7 @@ namespace Emby.Server.Implementations.Session
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_eventManager = eventManager;
|
||||
_userDataManager = userDataManager;
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
@ -209,6 +204,8 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
_eventManager.Publish(new SessionStartedEventArgs(info));
|
||||
|
||||
EventHelper.QueueEventIfNotNull(
|
||||
SessionStarted,
|
||||
this,
|
||||
@ -230,6 +227,8 @@ namespace Emby.Server.Implementations.Session
|
||||
},
|
||||
_logger);
|
||||
|
||||
_eventManager.Publish(new SessionEndedEventArgs(info));
|
||||
|
||||
info.Dispose();
|
||||
}
|
||||
|
||||
@ -667,22 +666,26 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
var eventArgs = new PlaybackProgressEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
// Nothing to save here
|
||||
// Fire events to inform plugins
|
||||
EventHelper.QueueEventIfNotNull(
|
||||
PlaybackStart,
|
||||
this,
|
||||
new PlaybackProgressEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
},
|
||||
eventArgs,
|
||||
_logger);
|
||||
|
||||
StartIdleCheckTimer();
|
||||
@ -750,23 +753,25 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
PlaybackProgress?.Invoke(
|
||||
this,
|
||||
new PlaybackProgressEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
PlaybackPositionTicks = session.PlayState.PositionTicks,
|
||||
MediaSourceId = session.PlayState.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
IsPaused = info.IsPaused,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
IsAutomated = isAutomated,
|
||||
Session = session
|
||||
});
|
||||
var eventArgs = new PlaybackProgressEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
PlaybackPositionTicks = session.PlayState.PositionTicks,
|
||||
MediaSourceId = session.PlayState.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
IsPaused = info.IsPaused,
|
||||
PlaySessionId = info.PlaySessionId,
|
||||
IsAutomated = isAutomated,
|
||||
Session = session
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
PlaybackProgress?.Invoke(this, eventArgs);
|
||||
|
||||
if (!isAutomated)
|
||||
{
|
||||
@ -943,23 +948,23 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
EventHelper.QueueEventIfNotNull(
|
||||
PlaybackStopped,
|
||||
this,
|
||||
new PlaybackStopEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
PlaybackPositionTicks = info.PositionTicks,
|
||||
PlayedToCompletion = playedToCompletion,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
},
|
||||
_logger);
|
||||
var eventArgs = new PlaybackStopEventArgs
|
||||
{
|
||||
Item = libraryItem,
|
||||
Users = users,
|
||||
PlaybackPositionTicks = info.PositionTicks,
|
||||
PlayedToCompletion = playedToCompletion,
|
||||
MediaSourceId = info.MediaSourceId,
|
||||
MediaInfo = info.Item,
|
||||
DeviceName = session.DeviceName,
|
||||
ClientName = session.Client,
|
||||
DeviceId = session.DeviceId,
|
||||
Session = session
|
||||
};
|
||||
|
||||
await _eventManager.PublishAsync(eventArgs).ConfigureAwait(false);
|
||||
|
||||
EventHelper.QueueEventIfNotNull(PlaybackStopped, this, eventArgs, _logger);
|
||||
}
|
||||
|
||||
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
|
||||
@ -1424,6 +1429,24 @@ namespace Emby.Server.Implementations.Session
|
||||
return AuthenticateNewSessionInternal(request, false);
|
||||
}
|
||||
|
||||
public Task<AuthenticationResult> AuthenticateQuickConnect(AuthenticationRequest request, string token)
|
||||
{
|
||||
var result = _authRepo.Get(new AuthenticationInfoQuery()
|
||||
{
|
||||
AccessToken = token,
|
||||
DeviceId = _appHost.SystemId,
|
||||
Limit = 1
|
||||
});
|
||||
|
||||
if (result.TotalRecordCount == 0)
|
||||
{
|
||||
throw new SecurityException("Unknown quick connect token");
|
||||
}
|
||||
|
||||
request.UserId = result.Items[0].UserId;
|
||||
return AuthenticateNewSessionInternal(request, false);
|
||||
}
|
||||
|
||||
private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
@ -4,9 +4,9 @@ using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -228,7 +228,7 @@ namespace Jellyfin.Api.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
private EventSubscriptionResponse ProcessEventRequest(IEventManager eventManager)
|
||||
private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
|
||||
{
|
||||
var subscriptionId = Request.Headers["SID"];
|
||||
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||
@ -239,17 +239,17 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (string.IsNullOrEmpty(notificationType))
|
||||
{
|
||||
return eventManager.RenewEventSubscription(
|
||||
return dlnaEventManager.RenewEventSubscription(
|
||||
subscriptionId,
|
||||
notificationType,
|
||||
timeoutString,
|
||||
callback);
|
||||
}
|
||||
|
||||
return eventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
||||
return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
||||
}
|
||||
|
||||
return eventManager.CancelEventSubscription(subscriptionId);
|
||||
return dlnaEventManager.CancelEventSubscription(subscriptionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1356,7 +1356,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0} {1} -map_metadata -1 -map_chapters -1 -threads {2} {3} {4} {5} -copyts -avoid_negative_ts disabled -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 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}\"",
|
||||
inputModifier,
|
||||
_encodingHelper.GetInputArgument(state, encodingOptions),
|
||||
threads,
|
||||
|
@ -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();
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
{
|
||||
@ -619,7 +618,7 @@ namespace Jellyfin.Api.Controllers
|
||||
[Authorize(Policy = Policies.Download)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult GetDownload([FromRoute] Guid itemId)
|
||||
public async Task<ActionResult> GetDownload([FromRoute] Guid itemId)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
if (item == null)
|
||||
@ -648,7 +647,7 @@ namespace Jellyfin.Api.Controllers
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
LogDownload(item, user, auth);
|
||||
await LogDownloadAsync(item, user, auth).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var path = item.Path;
|
||||
@ -861,17 +860,17 @@ namespace Jellyfin.Api.Controllers
|
||||
: item;
|
||||
}
|
||||
|
||||
private void LogDownload(BaseItem item, User user, AuthorizationInfo auth)
|
||||
private async Task LogDownloadAsync(BaseItem item, User user, AuthorizationInfo auth)
|
||||
{
|
||||
try
|
||||
{
|
||||
_activityManager.Create(new ActivityLog(
|
||||
await _activityManager.CreateAsync(new ActivityLog(
|
||||
string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
|
||||
"UserDownloadingContent",
|
||||
auth.UserId)
|
||||
{
|
||||
ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), auth.Client, auth.Device),
|
||||
});
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -120,10 +120,14 @@ namespace Jellyfin.Api.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
|
||||
var configuration = (BasePluginConfiguration?)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType, _serializerOptions)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
plugin.UpdateConfiguration(configuration);
|
||||
if (configuration != null)
|
||||
{
|
||||
plugin.UpdateConfiguration(configuration);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
154
Jellyfin.Api/Controllers/QuickConnectController.cs
Normal file
154
Jellyfin.Api/Controllers/QuickConnectController.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -216,6 +216,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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -127,7 +127,11 @@ namespace Jellyfin.Api.Helpers
|
||||
{
|
||||
// Since we're going to be setting properties on MediaSourceInfos that come out of _mediaSourceManager, we should clone it
|
||||
// Should we move this directly into MediaSourceManager?
|
||||
result.MediaSources = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
|
||||
var mediaSourcesClone = JsonSerializer.Deserialize<MediaSourceInfo[]>(JsonSerializer.SerializeToUtf8Bytes(mediaSources));
|
||||
if (mediaSourcesClone != null)
|
||||
{
|
||||
result.MediaSources = mediaSourcesClone;
|
||||
}
|
||||
|
||||
result.PlaySessionId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
@ -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)
|
||||
try
|
||||
{
|
||||
bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
bytesRead = source.Read(array, 0, array.Length);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
16
Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
Normal file
16
Jellyfin.Api/Models/UserDtos/QuickConnectDto.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Events;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Api.WebSocketListeners
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Events;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*************************************************************************/
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
81
Jellyfin.Data/Entities/Libraries/Artwork.cs
Normal file
81
Jellyfin.Data/Entities/Libraries/Artwork.cs
Normal file
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
28
Jellyfin.Data/Entities/Libraries/Book.cs
Normal file
28
Jellyfin.Data/Entities/Libraries/Book.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
55
Jellyfin.Data/Entities/Libraries/BookMetadata.cs
Normal file
55
Jellyfin.Data/Entities/Libraries/BookMetadata.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
102
Jellyfin.Data/Entities/Libraries/Chapter.cs
Normal file
102
Jellyfin.Data/Entities/Libraries/Chapter.cs
Normal file
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
55
Jellyfin.Data/Entities/Libraries/Collection.cs
Normal file
55
Jellyfin.Data/Entities/Libraries/Collection.cs
Normal file
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
94
Jellyfin.Data/Entities/Libraries/CollectionItem.cs
Normal file
94
Jellyfin.Data/Entities/Libraries/CollectionItem.cs
Normal file
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
67
Jellyfin.Data/Entities/Libraries/Company.cs
Normal file
67
Jellyfin.Data/Entities/Libraries/Company.cs
Normal file
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user