Merge remote-tracking branch 'upstream/master' into api-doc-css
This commit is contained in:
commit
bebb0afb52
|
@ -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)
|
||||
|
|
|
@ -13,7 +13,7 @@ namespace Emby.Dlna.Common
|
|||
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<Argument> ArgumentList { get; set; }
|
||||
public List<Argument> ArgumentList { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.Common
|
||||
{
|
||||
|
@ -17,7 +18,7 @@ namespace Emby.Dlna.Common
|
|||
|
||||
public bool SendsEvents { get; set; }
|
||||
|
||||
public string[] AllowedValues { get; set; }
|
||||
public IReadOnlyList<string> AllowedValues { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
|
@ -14,19 +13,4 @@ namespace Emby.Dlna
|
|||
return manager.GetConfiguration<DlnaOptions>("dlna");
|
||||
}
|
||||
}
|
||||
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new ConfigurationStore[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof (DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,22 +9,20 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.ConnectionManager
|
||||
{
|
||||
public class ConnectionManager : BaseService, IConnectionManager
|
||||
public class ConnectionManagerService : BaseService, IConnectionManager
|
||||
{
|
||||
private readonly IDlnaManager _dlna;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public ConnectionManager(
|
||||
public ConnectionManagerService(
|
||||
IDlnaManager dlna,
|
||||
IServerConfigurationManager config,
|
||||
ILogger<ConnectionManager> logger,
|
||||
ILogger<ConnectionManagerService> logger,
|
||||
IHttpClient httpClient)
|
||||
: base(logger, httpClient)
|
||||
{
|
||||
_dlna = dlna;
|
||||
_config = config;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -39,7 +37,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
var profile = _dlna.GetProfile(request.Headers) ??
|
||||
_dlna.GetDefaultProfile();
|
||||
|
||||
return new ControlHandler(_config, _logger, profile).ProcessControlRequestAsync(request);
|
||||
return new ControlHandler(_config, Logger, profile).ProcessControlRequestAsync(request);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -44,7 +44,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new string[]
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"OK",
|
||||
"ContentFormatMismatch",
|
||||
|
@ -67,7 +67,7 @@ namespace Emby.Dlna.ConnectionManager
|
|||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new string[]
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"Output",
|
||||
"Input"
|
||||
|
|
|
@ -19,7 +19,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
public class ContentDirectory : BaseService, IContentDirectory
|
||||
public class ContentDirectoryService : BaseService, IContentDirectory
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
@ -33,14 +33,14 @@ namespace Emby.Dlna.ContentDirectory
|
|||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
|
||||
public ContentDirectory(
|
||||
public ContentDirectoryService(
|
||||
IDlnaManager dlna,
|
||||
IUserDataManager userDataManager,
|
||||
IImageProcessor imageProcessor,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager config,
|
||||
IUserManager userManager,
|
||||
ILogger<ContentDirectory> logger,
|
||||
ILogger<ContentDirectoryService> logger,
|
||||
IHttpClient httpClient,
|
||||
ILocalizationManager localization,
|
||||
IMediaSourceManager mediaSourceManager,
|
|
@ -10,7 +10,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
public string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||
return new ServiceXmlBuilder().GetXml(
|
||||
new ServiceActionListBuilder().GetActions(),
|
||||
GetStateVariables());
|
||||
}
|
||||
|
||||
|
@ -101,7 +102,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
DataType = "string",
|
||||
SendsEvents = false,
|
||||
|
||||
AllowedValues = new string[]
|
||||
AllowedValues = new[]
|
||||
{
|
||||
"BrowseMetadata",
|
||||
"BrowseDirectChildren"
|
||||
|
|
|
@ -40,6 +40,11 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
public class ControlHandler : BaseControlHandler
|
||||
{
|
||||
private const string NsDc = "http://purl.org/dc/elements/1.1/";
|
||||
private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
|
||||
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
@ -47,11 +52,6 @@ namespace Emby.Dlna.ContentDirectory
|
|||
private readonly IUserViewManager _userViewManager;
|
||||
private readonly ITVSeriesManager _tvSeriesManager;
|
||||
|
||||
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
|
||||
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
|
||||
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
private readonly int _systemUpdateId;
|
||||
|
||||
private readonly DidlBuilder _didlBuilder;
|
||||
|
@ -181,7 +181,11 @@ namespace Emby.Dlna.ContentDirectory
|
|||
|
||||
userdata.PlaybackPositionTicks = TimeSpan.FromSeconds(newbookmark).Ticks;
|
||||
|
||||
_userDataManager.SaveUserData(_user, item, userdata, UserDataSaveReason.TogglePlayed,
|
||||
_userDataManager.SaveUserData(
|
||||
_user,
|
||||
item,
|
||||
userdata,
|
||||
UserDataSaveReason.TogglePlayed,
|
||||
CancellationToken.None);
|
||||
}
|
||||
|
||||
|
@ -253,7 +257,7 @@ namespace Emby.Dlna.ContentDirectory
|
|||
var id = sparams["ObjectID"];
|
||||
var flag = sparams["BrowseFlag"];
|
||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
|
||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||
|
||||
var provided = 0;
|
||||
|
||||
|
@ -286,18 +290,17 @@ namespace Emby.Dlna.ContentDirectory
|
|||
|
||||
using (var writer = XmlWriter.Create(builder, settings))
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
|
||||
|
||||
DidlBuilder.WriteXmlRootAttributes(_profile, writer);
|
||||
|
||||
var serverItem = GetItemFromObjectId(id);
|
||||
var item = serverItem.Item;
|
||||
|
||||
|
||||
if (string.Equals(flag, "BrowseMetadata", StringComparison.Ordinal))
|
||||
{
|
||||
totalCount = 1;
|
||||
|
@ -362,8 +365,8 @@ namespace Emby.Dlna.ContentDirectory
|
|||
|
||||
private void HandleSearch(XmlWriter xmlWriter, IDictionary<string, string> sparams, string deviceId)
|
||||
{
|
||||
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", ""));
|
||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", ""));
|
||||
var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", string.Empty));
|
||||
var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", string.Empty));
|
||||
var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*"));
|
||||
|
||||
// sort example: dc:title, dc:date
|
||||
|
@ -397,11 +400,11 @@ namespace Emby.Dlna.ContentDirectory
|
|||
|
||||
using (var writer = XmlWriter.Create(builder, settings))
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
|
||||
|
||||
DidlBuilder.WriteXmlRootAttributes(_profile, writer);
|
||||
|
||||
|
@ -783,11 +786,14 @@ namespace Emby.Dlna.ContentDirectory
|
|||
})
|
||||
.ToArray();
|
||||
|
||||
return ApplyPaging(new QueryResult<ServerItem>
|
||||
{
|
||||
Items = folders,
|
||||
TotalRecordCount = folders.Length
|
||||
}, startIndex, limit);
|
||||
return ApplyPaging(
|
||||
new QueryResult<ServerItem>
|
||||
{
|
||||
Items = folders,
|
||||
TotalRecordCount = folders.Length
|
||||
},
|
||||
startIndex,
|
||||
limit);
|
||||
}
|
||||
|
||||
private QueryResult<ServerItem> GetTvFolders(BaseItem item, User user, StubType? stubType, SortCriteria sort, int? startIndex, int? limit)
|
||||
|
@ -1135,14 +1141,16 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
query.OrderBy = Array.Empty<(string, SortOrder)>();
|
||||
|
||||
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
ParentId = parent?.Id ?? Guid.Empty,
|
||||
GroupItems = true
|
||||
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
var items = _userViewManager.GetLatestItems(
|
||||
new LatestItemsQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { nameof(Audio) },
|
||||
ParentId = parent?.Id ?? Guid.Empty,
|
||||
GroupItems = true
|
||||
},
|
||||
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
|
||||
return ToResult(items);
|
||||
}
|
||||
|
@ -1151,12 +1159,15 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
query.OrderBy = Array.Empty<(string, SortOrder)>();
|
||||
|
||||
var result = _tvSeriesManager.GetNextUp(new NextUpQuery
|
||||
{
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
UserId = query.User.Id
|
||||
}, new[] { parent }, query.DtoOptions);
|
||||
var result = _tvSeriesManager.GetNextUp(
|
||||
new NextUpQuery
|
||||
{
|
||||
Limit = query.Limit,
|
||||
StartIndex = query.StartIndex,
|
||||
UserId = query.User.Id
|
||||
},
|
||||
new[] { parent },
|
||||
query.DtoOptions);
|
||||
|
||||
return ToResult(result);
|
||||
}
|
||||
|
@ -1165,14 +1176,16 @@ namespace Emby.Dlna.ContentDirectory
|
|||
{
|
||||
query.OrderBy = Array.Empty<(string, SortOrder)>();
|
||||
|
||||
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||
ParentId = parent == null ? Guid.Empty : parent.Id,
|
||||
GroupItems = false
|
||||
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
var items = _userViewManager.GetLatestItems(
|
||||
new LatestItemsQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { typeof(Episode).Name },
|
||||
ParentId = parent == null ? Guid.Empty : parent.Id,
|
||||
GroupItems = false
|
||||
},
|
||||
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
|
||||
return ToResult(items);
|
||||
}
|
||||
|
@ -1183,13 +1196,14 @@ namespace Emby.Dlna.ContentDirectory
|
|||
|
||||
var items = _userViewManager.GetLatestItems(
|
||||
new LatestItemsQuery
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { nameof(Movie) },
|
||||
ParentId = parent?.Id ?? Guid.Empty,
|
||||
GroupItems = true
|
||||
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
{
|
||||
UserId = user.Id,
|
||||
Limit = 50,
|
||||
IncludeItemTypes = new[] { nameof(Movie) },
|
||||
ParentId = parent?.Id ?? Guid.Empty,
|
||||
GroupItems = true
|
||||
},
|
||||
query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
|
||||
|
||||
return ToResult(items);
|
||||
}
|
||||
|
@ -1354,44 +1368,4 @@ namespace Emby.Dlna.ContentDirectory
|
|||
return new ServerItem(_libraryManager.GetUserRootFolder());
|
||||
}
|
||||
}
|
||||
|
||||
internal class ServerItem
|
||||
{
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
public StubType? StubType { get; set; }
|
||||
|
||||
public ServerItem(BaseItem item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item is IItemByName && !(item is Folder))
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
Latest = 2,
|
||||
Playlists = 3,
|
||||
Albums = 4,
|
||||
AlbumArtists = 5,
|
||||
Artists = 6,
|
||||
Songs = 7,
|
||||
Genres = 8,
|
||||
FavoriteSongs = 9,
|
||||
FavoriteArtists = 10,
|
||||
FavoriteAlbums = 11,
|
||||
ContinueWatching = 12,
|
||||
Movies = 13,
|
||||
Collections = 14,
|
||||
Favorites = 15,
|
||||
NextUp = 16,
|
||||
Series = 17,
|
||||
FavoriteSeries = 18,
|
||||
FavoriteEpisodes = 19
|
||||
}
|
||||
}
|
||||
|
|
23
Emby.Dlna/ContentDirectory/ServerItem.cs
Normal file
23
Emby.Dlna/ContentDirectory/ServerItem.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
internal class ServerItem
|
||||
{
|
||||
public ServerItem(BaseItem item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
if (item is IItemByName && !(item is Folder))
|
||||
{
|
||||
StubType = Dlna.ContentDirectory.StubType.Folder;
|
||||
}
|
||||
}
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
public StubType? StubType { get; set; }
|
||||
}
|
||||
}
|
28
Emby.Dlna/ContentDirectory/StubType.cs
Normal file
28
Emby.Dlna/ContentDirectory/StubType.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.ContentDirectory
|
||||
{
|
||||
public enum StubType
|
||||
{
|
||||
Folder = 0,
|
||||
Latest = 2,
|
||||
Playlists = 3,
|
||||
Albums = 4,
|
||||
AlbumArtists = 5,
|
||||
Artists = 6,
|
||||
Songs = 7,
|
||||
Genres = 8,
|
||||
FavoriteSongs = 9,
|
||||
FavoriteArtists = 10,
|
||||
FavoriteAlbums = 11,
|
||||
ContinueWatching = 12,
|
||||
Movies = 13,
|
||||
Collections = 14,
|
||||
Favorites = 15,
|
||||
NextUp = 16,
|
||||
Series = 17,
|
||||
FavoriteSeries = 18,
|
||||
FavoriteEpisodes = 19
|
||||
}
|
||||
}
|
|
@ -7,17 +7,17 @@ namespace Emby.Dlna
|
|||
{
|
||||
public class ControlRequest
|
||||
{
|
||||
public IHeaderDictionary Headers { get; set; }
|
||||
public ControlRequest(IHeaderDictionary headers)
|
||||
{
|
||||
Headers = headers;
|
||||
}
|
||||
|
||||
public IHeaderDictionary Headers { get; }
|
||||
|
||||
public Stream InputXml { get; set; }
|
||||
|
||||
public string TargetServerUuId { get; set; }
|
||||
|
||||
public string RequestedUrl { get; set; }
|
||||
|
||||
public ControlRequest()
|
||||
{
|
||||
Headers = new HeaderDictionary();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace Emby.Dlna
|
|||
Headers = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public IDictionary<string, string> Headers { get; set; }
|
||||
public IDictionary<string, string> Headers { get; }
|
||||
|
||||
public string Xml { get; set; }
|
||||
|
||||
|
|
|
@ -34,12 +34,12 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
public class DidlBuilder
|
||||
{
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private const string NsDidl = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
private const string NsDc = "http://purl.org/dc/elements/1.1/";
|
||||
private const string NsUpnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
private const string NsDlna = "urn:schemas-dlna-org:metadata-1-0/";
|
||||
|
||||
private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
private const string NS_DC = "http://purl.org/dc/elements/1.1/";
|
||||
private const string NS_UPNP = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
private const string NS_DLNA = "urn:schemas-dlna-org:metadata-1-0/";
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly DeviceProfile _profile;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
|
@ -100,11 +100,11 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
// writer.WriteStartDocument();
|
||||
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "DIDL-Lite", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NS_DC);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP);
|
||||
writer.WriteAttributeString("xmlns", "dc", null, NsDc);
|
||||
writer.WriteAttributeString("xmlns", "dlna", null, NsDlna);
|
||||
writer.WriteAttributeString("xmlns", "upnp", null, NsUpnp);
|
||||
// didl.SetAttribute("xmlns:sec", NS_SEC);
|
||||
|
||||
WriteXmlRootAttributes(_profile, writer);
|
||||
|
@ -147,7 +147,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
var clientId = GetClientId(item, null);
|
||||
|
||||
writer.WriteStartElement(string.Empty, "item", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "item", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("restricted", "1");
|
||||
writer.WriteAttributeString("id", clientId);
|
||||
|
@ -207,7 +207,8 @@ namespace Emby.Dlna.Didl
|
|||
var targetWidth = streamInfo.TargetWidth;
|
||||
var targetHeight = streamInfo.TargetHeight;
|
||||
|
||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(streamInfo.Container,
|
||||
var contentFeatureList = new ContentFeatureBuilder(_profile).BuildVideoHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
targetWidth,
|
||||
|
@ -279,7 +280,7 @@ namespace Emby.Dlna.Didl
|
|||
}
|
||||
else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*");
|
||||
|
||||
|
@ -288,7 +289,7 @@ namespace Emby.Dlna.Didl
|
|||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
var protocolInfo = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"http-get:*:text/{0}:*",
|
||||
|
@ -304,7 +305,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
private void AddVideoResource(XmlWriter writer, Filter filter, string contentFeatures, StreamInfo streamInfo)
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
|
||||
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
||||
|
||||
|
@ -526,7 +527,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
|
||||
if (streamInfo == null)
|
||||
{
|
||||
|
@ -583,7 +584,8 @@ namespace Emby.Dlna.Didl
|
|||
writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture));
|
||||
}
|
||||
|
||||
var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container,
|
||||
var mediaProfile = _profile.GetAudioMediaProfile(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
targetChannels,
|
||||
targetAudioBitrate,
|
||||
|
@ -596,7 +598,8 @@ namespace Emby.Dlna.Didl
|
|||
? MimeTypes.GetMimeType(filename)
|
||||
: mediaProfile.MimeType;
|
||||
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(streamInfo.Container,
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile).BuildAudioHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
targetAudioBitrate,
|
||||
targetSampleRate,
|
||||
|
@ -627,7 +630,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
|
||||
{
|
||||
writer.WriteStartElement(string.Empty, "container", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "container", NsDidl);
|
||||
|
||||
writer.WriteAttributeString("restricted", "1");
|
||||
writer.WriteAttributeString("searchable", "1");
|
||||
|
@ -714,7 +717,7 @@ namespace Emby.Dlna.Didl
|
|||
// MediaMonkey for example won't display content without a title
|
||||
// if (filter.Contains("dc:title"))
|
||||
{
|
||||
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC);
|
||||
AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NsDc);
|
||||
}
|
||||
|
||||
WriteObjectClass(writer, item, itemStubType);
|
||||
|
@ -723,7 +726,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
if (item.PremiereDate.HasValue)
|
||||
{
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NS_DC);
|
||||
AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -731,13 +734,13 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
foreach (var genre in item.Genres)
|
||||
{
|
||||
AddValue(writer, "upnp", "genre", genre, NS_UPNP);
|
||||
AddValue(writer, "upnp", "genre", genre, NsUpnp);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var studio in item.Studios)
|
||||
{
|
||||
AddValue(writer, "upnp", "publisher", studio, NS_UPNP);
|
||||
AddValue(writer, "upnp", "publisher", studio, NsUpnp);
|
||||
}
|
||||
|
||||
if (!(item is Folder))
|
||||
|
@ -748,28 +751,29 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(desc))
|
||||
{
|
||||
AddValue(writer, "dc", "description", desc, NS_DC);
|
||||
AddValue(writer, "dc", "description", desc, NsDc);
|
||||
}
|
||||
}
|
||||
|
||||
// if (filter.Contains("upnp:longDescription"))
|
||||
//{
|
||||
// {
|
||||
// if (!string.IsNullOrWhiteSpace(item.Overview))
|
||||
// {
|
||||
// AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP);
|
||||
// AddValue(writer, "upnp", "longDescription", item.Overview, NsUpnp);
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(item.OfficialRating))
|
||||
{
|
||||
if (filter.Contains("dc:rating"))
|
||||
{
|
||||
AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC);
|
||||
AddValue(writer, "dc", "rating", item.OfficialRating, NsDc);
|
||||
}
|
||||
|
||||
if (filter.Contains("upnp:rating"))
|
||||
{
|
||||
AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP);
|
||||
AddValue(writer, "upnp", "rating", item.OfficialRating, NsUpnp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -781,7 +785,7 @@ namespace Emby.Dlna.Didl
|
|||
// More types here
|
||||
// http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs
|
||||
|
||||
writer.WriteStartElement("upnp", "class", NS_UPNP);
|
||||
writer.WriteStartElement("upnp", "class", NsUpnp);
|
||||
|
||||
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
||||
{
|
||||
|
@ -882,7 +886,7 @@ namespace Emby.Dlna.Didl
|
|||
var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
|
||||
?? PersonType.Actor;
|
||||
|
||||
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NS_UPNP);
|
||||
AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -896,8 +900,8 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
foreach (var artist in hasArtists.Artists)
|
||||
{
|
||||
AddValue(writer, "upnp", "artist", artist, NS_UPNP);
|
||||
AddValue(writer, "dc", "creator", artist, NS_DC);
|
||||
AddValue(writer, "upnp", "artist", artist, NsUpnp);
|
||||
AddValue(writer, "dc", "creator", artist, NsDc);
|
||||
|
||||
// If it doesn't support album artists (musicvideo), then tag as both
|
||||
if (hasAlbumArtists == null)
|
||||
|
@ -917,16 +921,16 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
if (!string.IsNullOrWhiteSpace(item.Album))
|
||||
{
|
||||
AddValue(writer, "upnp", "album", item.Album, NS_UPNP);
|
||||
AddValue(writer, "upnp", "album", item.Album, NsUpnp);
|
||||
}
|
||||
|
||||
if (item.IndexNumber.HasValue)
|
||||
{
|
||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
|
||||
AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
||||
|
||||
if (item is Episode)
|
||||
{
|
||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP);
|
||||
AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NsUpnp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -935,7 +939,7 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
try
|
||||
{
|
||||
writer.WriteStartElement("upnp", "artist", NS_UPNP);
|
||||
writer.WriteStartElement("upnp", "artist", NsUpnp);
|
||||
writer.WriteAttributeString("role", "AlbumArtist");
|
||||
|
||||
writer.WriteString(name);
|
||||
|
@ -971,14 +975,14 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, "jpg");
|
||||
|
||||
writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP);
|
||||
writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn);
|
||||
writer.WriteString(albumartUrlInfo.Url);
|
||||
writer.WriteStartElement("upnp", "albumArtURI", NsUpnp);
|
||||
writer.WriteAttributeString("dlna", "profileID", NsDlna, _profile.AlbumArtPn);
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
writer.WriteFullEndElement();
|
||||
|
||||
// TOOD: Remove these default values
|
||||
var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, "jpg");
|
||||
writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url);
|
||||
writer.WriteElementString("upnp", "icon", NsUpnp, iconUrlInfo.url);
|
||||
|
||||
if (!_profile.EnableAlbumArtInDidl)
|
||||
{
|
||||
|
@ -1021,12 +1025,12 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, format);
|
||||
|
||||
writer.WriteStartElement(string.Empty, "res", NS_DIDL);
|
||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||
|
||||
// Images must have a reported size or many clients (Bubble upnp), will only use the first thumbnail
|
||||
// rather than using a larger one when available
|
||||
var width = albumartUrlInfo.Width ?? maxWidth;
|
||||
var height = albumartUrlInfo.Height ?? maxHeight;
|
||||
var width = albumartUrlInfo.width ?? maxWidth;
|
||||
var height = albumartUrlInfo.height ?? maxHeight;
|
||||
|
||||
var contentFeatures = new ContentFeatureBuilder(_profile)
|
||||
.BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn);
|
||||
|
@ -1043,7 +1047,7 @@ namespace Emby.Dlna.Didl
|
|||
"resolution",
|
||||
string.Format(CultureInfo.InvariantCulture, "{0}x{1}", width, height));
|
||||
|
||||
writer.WriteString(albumartUrlInfo.Url);
|
||||
writer.WriteString(albumartUrlInfo.url);
|
||||
|
||||
writer.WriteFullEndElement();
|
||||
}
|
||||
|
@ -1139,7 +1143,6 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
// _imageProcessor.GetImageSize(item, imageInfo);
|
||||
width = null;
|
||||
height = null;
|
||||
}
|
||||
|
@ -1149,18 +1152,6 @@ namespace Emby.Dlna.Didl
|
|||
height = null;
|
||||
}
|
||||
|
||||
// try
|
||||
//{
|
||||
// var size = _imageProcessor.GetImageSize(imageInfo);
|
||||
|
||||
// width = size.Width;
|
||||
// height = size.Height;
|
||||
//}
|
||||
// catch
|
||||
//{
|
||||
|
||||
//}
|
||||
|
||||
var inputFormat = (Path.GetExtension(imageInfo.Path) ?? string.Empty)
|
||||
.TrimStart('.')
|
||||
.Replace("jpeg", "jpg", StringComparison.OrdinalIgnoreCase);
|
||||
|
@ -1177,30 +1168,6 @@ namespace Emby.Dlna.Didl
|
|||
};
|
||||
}
|
||||
|
||||
private class ImageDownloadInfo
|
||||
{
|
||||
internal Guid ItemId;
|
||||
internal string ImageTag;
|
||||
internal ImageType Type;
|
||||
|
||||
internal int? Width;
|
||||
internal int? Height;
|
||||
|
||||
internal bool IsDirectStream;
|
||||
|
||||
internal string Format;
|
||||
|
||||
internal ItemImageInfo ItemImageInfo;
|
||||
}
|
||||
|
||||
private class ImageUrlInfo
|
||||
{
|
||||
internal string Url;
|
||||
|
||||
internal int? Width;
|
||||
internal int? Height;
|
||||
}
|
||||
|
||||
public static string GetClientId(BaseItem item, StubType? stubType)
|
||||
{
|
||||
return GetClientId(item.Id, stubType);
|
||||
|
@ -1218,7 +1185,7 @@ namespace Emby.Dlna.Didl
|
|||
return id;
|
||||
}
|
||||
|
||||
private ImageUrlInfo GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
private (string url, int? width, int? height) GetImageUrl(ImageDownloadInfo info, int maxWidth, int maxHeight, string format)
|
||||
{
|
||||
var url = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
|
@ -1256,12 +1223,26 @@ namespace Emby.Dlna.Didl
|
|||
// just lie
|
||||
info.IsDirectStream = true;
|
||||
|
||||
return new ImageUrlInfo
|
||||
{
|
||||
Url = url,
|
||||
Width = width,
|
||||
Height = height
|
||||
};
|
||||
return (url, width, height);
|
||||
}
|
||||
|
||||
private class ImageDownloadInfo
|
||||
{
|
||||
internal Guid ItemId { get; set; }
|
||||
|
||||
internal string ImageTag { get; set; }
|
||||
|
||||
internal ImageType Type { get; set; }
|
||||
|
||||
internal int? Width { get; set; }
|
||||
|
||||
internal int? Height { get; set; }
|
||||
|
||||
internal bool IsDirectStream { get; set; }
|
||||
|
||||
internal string Format { get; set; }
|
||||
|
||||
internal ItemImageInfo ItemImageInfo { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,9 +23,7 @@ namespace Emby.Dlna.Didl
|
|||
|
||||
public bool Contains(string field)
|
||||
{
|
||||
// Don't bother with this. Some clients (media monkey) use the filter and then don't display very well when very little data comes back.
|
||||
return true;
|
||||
// return _all || ListHelper.ContainsIgnoreCase(_fields, field);
|
||||
return _all || Array.Exists(_fields, x => x.Equals(field, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CA1305
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
@ -29,7 +30,6 @@ namespace Emby.Dlna.Didl
|
|||
{
|
||||
}
|
||||
|
||||
|
||||
public StringWriterWithEncoding(Encoding encoding)
|
||||
{
|
||||
_encoding = encoding;
|
||||
|
|
24
Emby.Dlna/DlnaConfigurationFactory.cs
Normal file
24
Emby.Dlna/DlnaConfigurationFactory.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
#nullable enable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Emby.Dlna.Configuration;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public class DlnaConfigurationFactory : IConfigurationFactory
|
||||
{
|
||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new ConfigurationStore
|
||||
{
|
||||
Key = "dlna",
|
||||
ConfigurationType = typeof(DlnaOptions)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -54,11 +54,15 @@ namespace Emby.Dlna
|
|||
_appHost = appHost;
|
||||
}
|
||||
|
||||
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
|
||||
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
|
||||
public async Task InitProfilesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExtractSystemProfilesAsync();
|
||||
await ExtractSystemProfilesAsync().ConfigureAwait(false);
|
||||
LoadProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -240,7 +244,7 @@ namespace Emby.Dlna
|
|||
}
|
||||
else
|
||||
{
|
||||
var headerString = string.Join(", ", headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray());
|
||||
var headerString = string.Join(", ", headers.Select(i => string.Format(CultureInfo.InvariantCulture, "{0}={1}", i.Key, i.Value)));
|
||||
_logger.LogDebug("No matching device profile found. {0}", headerString);
|
||||
}
|
||||
|
||||
|
@ -280,10 +284,6 @@ namespace Emby.Dlna
|
|||
return false;
|
||||
}
|
||||
|
||||
private string UserProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
||||
|
||||
private string SystemProfilesPath => Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
||||
|
||||
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
|
||||
{
|
||||
try
|
||||
|
@ -495,8 +495,8 @@ namespace Emby.Dlna
|
|||
/// Recreates the object using serialization, to ensure it's not a subclass.
|
||||
/// If it's a subclass it may not serlialize properly to xml (different root element tag name).
|
||||
/// </summary>
|
||||
/// <param name="profile"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="profile">The device profile.</param>
|
||||
/// <returns>The reserialized device profile.</returns>
|
||||
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
||||
{
|
||||
if (profile.GetType() == typeof(DeviceProfile))
|
||||
|
@ -509,13 +509,6 @@ namespace Emby.Dlna
|
|||
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal DeviceProfileInfo Info { get; set; }
|
||||
|
||||
internal string Path { get; set; }
|
||||
}
|
||||
|
||||
public string GetServerDescriptionXml(IHeaderDictionary headers, string serverUuId, string serverAddress)
|
||||
{
|
||||
var profile = GetProfile(headers) ??
|
||||
|
@ -540,7 +533,15 @@ namespace Emby.Dlna
|
|||
Stream = _assembly.GetManifestResourceStream(resource)
|
||||
};
|
||||
}
|
||||
|
||||
private class InternalProfileInfo
|
||||
{
|
||||
internal DeviceProfileInfo Info { get; set; }
|
||||
|
||||
internal string Path { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
class DlnaProfileEntryPoint : IServerEntryPoint
|
||||
{
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<TargetFramework>netstandard2.1</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Code Analyzers-->
|
||||
|
|
|
@ -15,6 +15,6 @@ namespace Emby.Dlna
|
|||
|
||||
public string ContentType { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; set; }
|
||||
public Dictionary<string, string> Headers { get; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
@ -22,7 +22,9 @@ namespace Emby.Dlna.Eventing
|
|||
private readonly ILogger _logger;
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
public EventManager(ILogger logger, IHttpClient httpClient)
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
|
||||
public DlnaEventManager(ILogger logger, IHttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
|
@ -58,7 +60,8 @@ namespace Emby.Dlna.Eventing
|
|||
var timeout = ParseTimeout(requestedTimeoutString) ?? 300;
|
||||
var id = "uuid:" + Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
_logger.LogDebug("Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
_logger.LogDebug(
|
||||
"Creating event subscription for {0} with timeout of {1} to {2}",
|
||||
notificationType,
|
||||
timeout,
|
||||
callbackUrl);
|
||||
|
@ -94,7 +97,7 @@ namespace Emby.Dlna.Eventing
|
|||
{
|
||||
_logger.LogDebug("Cancelling event subscription {0}", subscriptionId);
|
||||
|
||||
_subscriptions.TryRemove(subscriptionId, out EventSubscription sub);
|
||||
_subscriptions.TryRemove(subscriptionId, out _);
|
||||
|
||||
return new EventSubscriptionResponse
|
||||
{
|
||||
|
@ -103,7 +106,6 @@ namespace Emby.Dlna.Eventing
|
|||
};
|
||||
}
|
||||
|
||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||
private EventSubscriptionResponse GetEventSubscriptionResponse(string subscriptionId, string requestedTimeoutString, int timeoutSeconds)
|
||||
{
|
||||
var response = new EventSubscriptionResponse
|
|
@ -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,22 +2,32 @@
|
|||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IEventManager
|
||||
public interface IDlnaEventManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Cancels the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CancelEventSubscription(string subscriptionId);
|
||||
|
||||
/// <summary>
|
||||
/// Renews the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The subscription identifier.</param>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse RenewEventSubscription(string subscriptionId, string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the event subscription.
|
||||
/// </summary>
|
||||
/// <param name="notificationType">The notification type.</param>
|
||||
/// <param name="requestedTimeoutString">The requested timeout as a sting.</param>
|
||||
/// <param name="callbackUrl">The callback url.</param>
|
||||
/// <returns>The response.</returns>
|
||||
EventSubscriptionResponse CreateEventSubscription(string notificationType, string requestedTimeoutString, string callbackUrl);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace Emby.Dlna
|
||||
{
|
||||
public interface IMediaReceiverRegistrar : IEventManager, IUpnpService
|
||||
public interface IMediaReceiverRegistrar : IDlnaEventManager, IUpnpService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ using OperatingSystem = MediaBrowser.Common.System.OperatingSystem;
|
|||
|
||||
namespace Emby.Dlna.Main
|
||||
{
|
||||
public class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
||||
public sealed class DlnaEntryPoint : IServerEntryPoint, IRunBeforeStartup
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
private readonly ILogger<DlnaEntryPoint> _logger;
|
||||
|
@ -54,13 +54,7 @@ namespace Emby.Dlna.Main
|
|||
private SsdpDevicePublisher _publisher;
|
||||
private ISsdpCommunicationsServer _communicationsServer;
|
||||
|
||||
public IContentDirectory ContentDirectory { get; private set; }
|
||||
|
||||
public IConnectionManager ConnectionManager { get; private set; }
|
||||
|
||||
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||
|
||||
public static DlnaEntryPoint Current;
|
||||
private bool _disposed;
|
||||
|
||||
public DlnaEntryPoint(
|
||||
IServerConfigurationManager config,
|
||||
|
@ -99,14 +93,14 @@ namespace Emby.Dlna.Main
|
|||
_networkManager = networkManager;
|
||||
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
|
||||
|
||||
ContentDirectory = new ContentDirectory.ContentDirectory(
|
||||
ContentDirectory = new ContentDirectory.ContentDirectoryService(
|
||||
dlnaManager,
|
||||
userDataManager,
|
||||
imageProcessor,
|
||||
libraryManager,
|
||||
config,
|
||||
userManager,
|
||||
loggerFactory.CreateLogger<ContentDirectory.ContentDirectory>(),
|
||||
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
|
||||
httpClient,
|
||||
localizationManager,
|
||||
mediaSourceManager,
|
||||
|
@ -114,19 +108,27 @@ namespace Emby.Dlna.Main
|
|||
mediaEncoder,
|
||||
tvSeriesManager);
|
||||
|
||||
ConnectionManager = new ConnectionManager.ConnectionManager(
|
||||
ConnectionManager = new ConnectionManager.ConnectionManagerService(
|
||||
dlnaManager,
|
||||
config,
|
||||
loggerFactory.CreateLogger<ConnectionManager.ConnectionManager>(),
|
||||
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
|
||||
httpClient);
|
||||
|
||||
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrar(
|
||||
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrar>(),
|
||||
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
|
||||
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
|
||||
httpClient,
|
||||
config);
|
||||
Current = this;
|
||||
}
|
||||
|
||||
public static DlnaEntryPoint Current { get; private set; }
|
||||
|
||||
public IContentDirectory ContentDirectory { get; private set; }
|
||||
|
||||
public IConnectionManager ConnectionManager { get; private set; }
|
||||
|
||||
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
|
@ -399,8 +401,24 @@ namespace Emby.Dlna.Main
|
|||
}
|
||||
}
|
||||
|
||||
public void DisposeDevicePublisher()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DisposeDevicePublisher();
|
||||
DisposePlayToManager();
|
||||
DisposeDeviceDiscovery();
|
||||
|
@ -416,16 +434,8 @@ namespace Emby.Dlna.Main
|
|||
ConnectionManager = null;
|
||||
MediaReceiverRegistrar = null;
|
||||
Current = null;
|
||||
}
|
||||
|
||||
public void DisposeDevicePublisher()
|
||||
{
|
||||
if (_publisher != null)
|
||||
{
|
||||
_logger.LogInformation("Disposing SsdpDevicePublisher");
|
||||
_publisher.Dispose();
|
||||
_publisher = null;
|
||||
}
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.MediaReceiverRegistrar
|
||||
{
|
||||
public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar
|
||||
public class MediaReceiverRegistrarService : BaseService, IMediaReceiverRegistrar
|
||||
{
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
public MediaReceiverRegistrar(
|
||||
ILogger<MediaReceiverRegistrar> logger,
|
||||
public MediaReceiverRegistrarService(
|
||||
ILogger<MediaReceiverRegistrarService> logger,
|
||||
IHttpClient httpClient,
|
||||
IServerConfigurationManager config)
|
||||
: base(logger, httpClient)
|
|
@ -10,7 +10,8 @@ namespace Emby.Dlna.MediaReceiverRegistrar
|
|||
{
|
||||
public string GetXml()
|
||||
{
|
||||
return new ServiceXmlBuilder().GetXml(new ServiceActionListBuilder().GetActions(),
|
||||
return new ServiceXmlBuilder().GetXml(
|
||||
new ServiceActionListBuilder().GetActions(),
|
||||
GetStateVariables());
|
||||
}
|
||||
|
||||
|
|
|
@ -19,15 +19,40 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class Device : IDisposable
|
||||
{
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly object _timerLock = new object();
|
||||
private Timer _timer;
|
||||
private int _muteVol;
|
||||
private int _volume;
|
||||
private DateTime _lastVolumeRefresh;
|
||||
private bool _volumeRefreshActive;
|
||||
private int _connectFailureCount;
|
||||
private bool _disposed;
|
||||
|
||||
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger)
|
||||
{
|
||||
Properties = deviceProperties;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
||||
|
||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
||||
|
||||
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
|
||||
|
||||
public event EventHandler<MediaChangedEventArgs> MediaChanged;
|
||||
|
||||
public DeviceInfo Properties { get; set; }
|
||||
|
||||
private int _muteVol;
|
||||
public bool IsMuted { get; set; }
|
||||
|
||||
private int _volume;
|
||||
|
||||
public int Volume
|
||||
{
|
||||
get
|
||||
|
@ -43,29 +68,21 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public TimeSpan Position { get; set; } = TimeSpan.FromSeconds(0);
|
||||
|
||||
public TRANSPORTSTATE TransportState { get; private set; }
|
||||
public TransportState TransportState { get; private set; }
|
||||
|
||||
public bool IsPlaying => TransportState == TRANSPORTSTATE.PLAYING;
|
||||
public bool IsPlaying => TransportState == TransportState.Playing;
|
||||
|
||||
public bool IsPaused => TransportState == TRANSPORTSTATE.PAUSED || TransportState == TRANSPORTSTATE.PAUSED_PLAYBACK;
|
||||
public bool IsPaused => TransportState == TransportState.Paused || TransportState == TransportState.PausedPlayback;
|
||||
|
||||
public bool IsStopped => TransportState == TRANSPORTSTATE.STOPPED;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
public bool IsStopped => TransportState == TransportState.Stopped;
|
||||
|
||||
public Action OnDeviceUnavailable { get; set; }
|
||||
|
||||
public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config)
|
||||
{
|
||||
Properties = deviceProperties;
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
_config = config;
|
||||
}
|
||||
private TransportCommands AvCommands { get; set; }
|
||||
|
||||
private TransportCommands RendererCommands { get; set; }
|
||||
|
||||
public UBaseObject CurrentMediaInfo { get; private set; }
|
||||
|
||||
public void Start()
|
||||
{
|
||||
|
@ -73,8 +90,6 @@ namespace Emby.Dlna.PlayTo
|
|||
_timer = new Timer(TimerCallback, null, 1000, Timeout.Infinite);
|
||||
}
|
||||
|
||||
private DateTime _lastVolumeRefresh;
|
||||
private bool _volumeRefreshActive;
|
||||
private Task RefreshVolumeIfNeeded()
|
||||
{
|
||||
if (_volumeRefreshActive
|
||||
|
@ -105,7 +120,6 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private readonly object _timerLock = new object();
|
||||
private void RestartTimer(bool immediate = false)
|
||||
{
|
||||
lock (_timerLock)
|
||||
|
@ -233,6 +247,9 @@ namespace Emby.Dlna.PlayTo
|
|||
/// <summary>
|
||||
/// Sets volume on a scale of 0-100.
|
||||
/// </summary>
|
||||
/// <param name="value">The volume on a scale of 0-100.</param>
|
||||
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task SetVolume(int value, CancellationToken cancellationToken)
|
||||
{
|
||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
@ -275,7 +292,7 @@ namespace Emby.Dlna.PlayTo
|
|||
throw new InvalidOperationException("Unable to find service");
|
||||
}
|
||||
|
||||
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format("{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
||||
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
RestartTimer(true);
|
||||
|
@ -285,7 +302,7 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
url = url.Replace("&", "&");
|
||||
url = url.Replace("&", "&", StringComparison.Ordinal);
|
||||
|
||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||
|
||||
|
@ -297,8 +314,8 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
var dictionary = new Dictionary<string, string>
|
||||
{
|
||||
{"CurrentURI", url},
|
||||
{"CurrentURIMetaData", CreateDidlMeta(metaData)}
|
||||
{ "CurrentURI", url },
|
||||
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
|
||||
};
|
||||
|
||||
var service = GetAvTransportService();
|
||||
|
@ -401,13 +418,11 @@ namespace Emby.Dlna.PlayTo
|
|||
await new SsdpHttpClient(_httpClient).SendCommandAsync(Properties.BaseUrl, service, command.Name, avCommands.BuildPost(command, service.ServiceType, 1))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
TransportState = TRANSPORTSTATE.PAUSED;
|
||||
TransportState = TransportState.Paused;
|
||||
|
||||
RestartTimer(true);
|
||||
}
|
||||
|
||||
private int _connectFailureCount;
|
||||
|
||||
private async void TimerCallback(object sender)
|
||||
{
|
||||
if (_disposed)
|
||||
|
@ -436,7 +451,7 @@ namespace Emby.Dlna.PlayTo
|
|||
if (transportState.HasValue)
|
||||
{
|
||||
// If we're not playing anything no need to get additional data
|
||||
if (transportState.Value == TRANSPORTSTATE.STOPPED)
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
{
|
||||
UpdateMediaInfo(null, transportState.Value);
|
||||
}
|
||||
|
@ -465,7 +480,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
|
||||
// If we're not playing anything make sure we don't get data more often than neccessry to keep the Session alive
|
||||
if (transportState.Value == TRANSPORTSTATE.STOPPED)
|
||||
if (transportState.Value == TransportState.Stopped)
|
||||
{
|
||||
RestartTimerInactive();
|
||||
}
|
||||
|
@ -539,7 +554,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return;
|
||||
}
|
||||
|
||||
var volume = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
|
||||
var volume = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetVolumeResponse").Select(i => i.Element("CurrentVolume")).FirstOrDefault(i => i != null);
|
||||
var volumeValue = volume?.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(volumeValue))
|
||||
|
@ -589,14 +604,14 @@ namespace Emby.Dlna.PlayTo
|
|||
return;
|
||||
}
|
||||
|
||||
var valueNode = result.Document.Descendants(uPnpNamespaces.RenderingControl + "GetMuteResponse")
|
||||
var valueNode = result.Document.Descendants(UPnpNamespaces.RenderingControl + "GetMuteResponse")
|
||||
.Select(i => i.Element("CurrentMute"))
|
||||
.FirstOrDefault(i => i != null);
|
||||
|
||||
IsMuted = string.Equals(valueNode?.Value, "1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task<TRANSPORTSTATE?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<TransportState?> GetTransportInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetTransportInfo");
|
||||
if (command == null)
|
||||
|
@ -623,12 +638,12 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
|
||||
var transportState =
|
||||
result.Document.Descendants(uPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
|
||||
result.Document.Descendants(UPnpNamespaces.AvTransport + "GetTransportInfoResponse").Select(i => i.Element("CurrentTransportState")).FirstOrDefault(i => i != null);
|
||||
|
||||
var transportStateValue = transportState?.Value;
|
||||
|
||||
if (transportStateValue != null
|
||||
&& Enum.TryParse(transportStateValue, true, out TRANSPORTSTATE state))
|
||||
&& Enum.TryParse(transportStateValue, true, out TransportState state))
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
@ -636,7 +651,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
private async Task<uBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
|
||||
if (command == null)
|
||||
|
@ -671,7 +686,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
var e = track.Element(uPnpNamespaces.items) ?? track;
|
||||
var e = track.Element(UPnpNamespaces.Items) ?? track;
|
||||
|
||||
var elementString = (string)e;
|
||||
|
||||
|
@ -687,13 +702,13 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
e = track.Element(uPnpNamespaces.items) ?? track;
|
||||
e = track.Element(UPnpNamespaces.Items) ?? track;
|
||||
|
||||
elementString = (string)e;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(elementString))
|
||||
{
|
||||
return new uBaseObject
|
||||
return new UBaseObject
|
||||
{
|
||||
Url = elementString
|
||||
};
|
||||
|
@ -702,7 +717,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool, uBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
||||
if (command == null)
|
||||
|
@ -731,11 +746,11 @@ namespace Emby.Dlna.PlayTo
|
|||
return (false, null);
|
||||
}
|
||||
|
||||
var trackUriElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
|
||||
var trackUri = trackUriElem == null ? null : trackUriElem.Value;
|
||||
var trackUriElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackURI")).FirstOrDefault(i => i != null);
|
||||
var trackUri = trackUriElem?.Value;
|
||||
|
||||
var durationElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
|
||||
var duration = durationElem == null ? null : durationElem.Value;
|
||||
var durationElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("TrackDuration")).FirstOrDefault(i => i != null);
|
||||
var duration = durationElem?.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(duration)
|
||||
&& !string.Equals(duration, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -747,8 +762,8 @@ namespace Emby.Dlna.PlayTo
|
|||
Duration = null;
|
||||
}
|
||||
|
||||
var positionElem = result.Document.Descendants(uPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
|
||||
var position = positionElem == null ? null : positionElem.Value;
|
||||
var positionElem = result.Document.Descendants(UPnpNamespaces.AvTransport + "GetPositionInfoResponse").Select(i => i.Element("RelTime")).FirstOrDefault(i => i != null);
|
||||
var position = positionElem?.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(position) && !string.Equals(position, "NOT_IMPLEMENTED", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -787,7 +802,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return (true, null);
|
||||
}
|
||||
|
||||
var e = uPnpResponse.Element(uPnpNamespaces.items);
|
||||
var e = uPnpResponse.Element(UPnpNamespaces.Items);
|
||||
|
||||
var uTrack = CreateUBaseObject(e, trackUri);
|
||||
|
||||
|
@ -819,7 +834,7 @@ namespace Emby.Dlna.PlayTo
|
|||
// some devices send back invalid xml
|
||||
try
|
||||
{
|
||||
return XElement.Parse(xml.Replace("&", "&"));
|
||||
return XElement.Parse(xml.Replace("&", "&", StringComparison.Ordinal));
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
|
@ -828,27 +843,27 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
private static uBaseObject CreateUBaseObject(XElement container, string trackUri)
|
||||
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
var url = container.GetValue(uPnpNamespaces.Res);
|
||||
var url = container.GetValue(UPnpNamespaces.Res);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
url = trackUri;
|
||||
}
|
||||
|
||||
return new uBaseObject
|
||||
return new UBaseObject
|
||||
{
|
||||
Id = container.GetAttributeValue(uPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(uPnpNamespaces.title),
|
||||
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
|
||||
SecondText = "",
|
||||
Id = container.GetAttributeValue(UPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(UPnpNamespaces.Title),
|
||||
IconUrl = container.GetValue(UPnpNamespaces.Artwork),
|
||||
SecondText = string.Empty,
|
||||
Url = url,
|
||||
ProtocolInfo = GetProtocolInfo(container),
|
||||
MetaData = container.ToString()
|
||||
|
@ -862,11 +877,11 @@ namespace Emby.Dlna.PlayTo
|
|||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
var resElement = container.Element(uPnpNamespaces.Res);
|
||||
var resElement = container.Element(UPnpNamespaces.Res);
|
||||
|
||||
if (resElement != null)
|
||||
{
|
||||
var info = resElement.Attribute(uPnpNamespaces.ProtocolInfo);
|
||||
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
|
||||
|
||||
if (info != null && !string.IsNullOrWhiteSpace(info.Value))
|
||||
{
|
||||
|
@ -941,12 +956,12 @@ namespace Emby.Dlna.PlayTo
|
|||
return url;
|
||||
}
|
||||
|
||||
if (!url.Contains("/"))
|
||||
if (!url.Contains('/', StringComparison.Ordinal))
|
||||
{
|
||||
url = "/dmr/" + url;
|
||||
}
|
||||
|
||||
if (!url.StartsWith("/"))
|
||||
if (!url.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
url = "/" + url;
|
||||
}
|
||||
|
@ -954,11 +969,7 @@ namespace Emby.Dlna.PlayTo
|
|||
return baseUrl + url;
|
||||
}
|
||||
|
||||
private TransportCommands AvCommands { get; set; }
|
||||
|
||||
private TransportCommands RendererCommands { get; set; }
|
||||
|
||||
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, CancellationToken cancellationToken)
|
||||
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, ILogger logger, CancellationToken cancellationToken)
|
||||
{
|
||||
var ssdpHttpClient = new SsdpHttpClient(httpClient);
|
||||
|
||||
|
@ -966,13 +977,13 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
var friendlyNames = new List<string>();
|
||||
|
||||
var name = document.Descendants(uPnpNamespaces.ud.GetName("friendlyName")).FirstOrDefault();
|
||||
var name = document.Descendants(UPnpNamespaces.Ud.GetName("friendlyName")).FirstOrDefault();
|
||||
if (name != null && !string.IsNullOrWhiteSpace(name.Value))
|
||||
{
|
||||
friendlyNames.Add(name.Value);
|
||||
}
|
||||
|
||||
var room = document.Descendants(uPnpNamespaces.ud.GetName("roomName")).FirstOrDefault();
|
||||
var room = document.Descendants(UPnpNamespaces.Ud.GetName("roomName")).FirstOrDefault();
|
||||
if (room != null && !string.IsNullOrWhiteSpace(room.Value))
|
||||
{
|
||||
friendlyNames.Add(room.Value);
|
||||
|
@ -981,77 +992,77 @@ namespace Emby.Dlna.PlayTo
|
|||
var deviceProperties = new DeviceInfo()
|
||||
{
|
||||
Name = string.Join(" ", friendlyNames),
|
||||
BaseUrl = string.Format("http://{0}:{1}", url.Host, url.Port)
|
||||
BaseUrl = string.Format(CultureInfo.InvariantCulture, "http://{0}:{1}", url.Host, url.Port)
|
||||
};
|
||||
|
||||
var model = document.Descendants(uPnpNamespaces.ud.GetName("modelName")).FirstOrDefault();
|
||||
var model = document.Descendants(UPnpNamespaces.Ud.GetName("modelName")).FirstOrDefault();
|
||||
if (model != null)
|
||||
{
|
||||
deviceProperties.ModelName = model.Value;
|
||||
}
|
||||
|
||||
var modelNumber = document.Descendants(uPnpNamespaces.ud.GetName("modelNumber")).FirstOrDefault();
|
||||
var modelNumber = document.Descendants(UPnpNamespaces.Ud.GetName("modelNumber")).FirstOrDefault();
|
||||
if (modelNumber != null)
|
||||
{
|
||||
deviceProperties.ModelNumber = modelNumber.Value;
|
||||
}
|
||||
|
||||
var uuid = document.Descendants(uPnpNamespaces.ud.GetName("UDN")).FirstOrDefault();
|
||||
var uuid = document.Descendants(UPnpNamespaces.Ud.GetName("UDN")).FirstOrDefault();
|
||||
if (uuid != null)
|
||||
{
|
||||
deviceProperties.UUID = uuid.Value;
|
||||
}
|
||||
|
||||
var manufacturer = document.Descendants(uPnpNamespaces.ud.GetName("manufacturer")).FirstOrDefault();
|
||||
var manufacturer = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturer")).FirstOrDefault();
|
||||
if (manufacturer != null)
|
||||
{
|
||||
deviceProperties.Manufacturer = manufacturer.Value;
|
||||
}
|
||||
|
||||
var manufacturerUrl = document.Descendants(uPnpNamespaces.ud.GetName("manufacturerURL")).FirstOrDefault();
|
||||
var manufacturerUrl = document.Descendants(UPnpNamespaces.Ud.GetName("manufacturerURL")).FirstOrDefault();
|
||||
if (manufacturerUrl != null)
|
||||
{
|
||||
deviceProperties.ManufacturerUrl = manufacturerUrl.Value;
|
||||
}
|
||||
|
||||
var presentationUrl = document.Descendants(uPnpNamespaces.ud.GetName("presentationURL")).FirstOrDefault();
|
||||
var presentationUrl = document.Descendants(UPnpNamespaces.Ud.GetName("presentationURL")).FirstOrDefault();
|
||||
if (presentationUrl != null)
|
||||
{
|
||||
deviceProperties.PresentationUrl = presentationUrl.Value;
|
||||
}
|
||||
|
||||
var modelUrl = document.Descendants(uPnpNamespaces.ud.GetName("modelURL")).FirstOrDefault();
|
||||
var modelUrl = document.Descendants(UPnpNamespaces.Ud.GetName("modelURL")).FirstOrDefault();
|
||||
if (modelUrl != null)
|
||||
{
|
||||
deviceProperties.ModelUrl = modelUrl.Value;
|
||||
}
|
||||
|
||||
var serialNumber = document.Descendants(uPnpNamespaces.ud.GetName("serialNumber")).FirstOrDefault();
|
||||
var serialNumber = document.Descendants(UPnpNamespaces.Ud.GetName("serialNumber")).FirstOrDefault();
|
||||
if (serialNumber != null)
|
||||
{
|
||||
deviceProperties.SerialNumber = serialNumber.Value;
|
||||
}
|
||||
|
||||
var modelDescription = document.Descendants(uPnpNamespaces.ud.GetName("modelDescription")).FirstOrDefault();
|
||||
var modelDescription = document.Descendants(UPnpNamespaces.Ud.GetName("modelDescription")).FirstOrDefault();
|
||||
if (modelDescription != null)
|
||||
{
|
||||
deviceProperties.ModelDescription = modelDescription.Value;
|
||||
}
|
||||
|
||||
var icon = document.Descendants(uPnpNamespaces.ud.GetName("icon")).FirstOrDefault();
|
||||
var icon = document.Descendants(UPnpNamespaces.Ud.GetName("icon")).FirstOrDefault();
|
||||
if (icon != null)
|
||||
{
|
||||
deviceProperties.Icon = CreateIcon(icon);
|
||||
}
|
||||
|
||||
foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList")))
|
||||
foreach (var services in document.Descendants(UPnpNamespaces.Ud.GetName("serviceList")))
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var servicesList = services.Descendants(uPnpNamespaces.ud.GetName("service"));
|
||||
var servicesList = services.Descendants(UPnpNamespaces.Ud.GetName("service"));
|
||||
if (servicesList == null)
|
||||
{
|
||||
continue;
|
||||
|
@ -1068,10 +1079,9 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return new Device(deviceProperties, httpClient, logger, config);
|
||||
return new Device(deviceProperties, httpClient, logger);
|
||||
}
|
||||
|
||||
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
if (element == null)
|
||||
|
@ -1079,11 +1089,11 @@ namespace Emby.Dlna.PlayTo
|
|||
throw new ArgumentNullException(nameof(element));
|
||||
}
|
||||
|
||||
var mimeType = element.GetDescendantValue(uPnpNamespaces.ud.GetName("mimetype"));
|
||||
var width = element.GetDescendantValue(uPnpNamespaces.ud.GetName("width"));
|
||||
var height = element.GetDescendantValue(uPnpNamespaces.ud.GetName("height"));
|
||||
var depth = element.GetDescendantValue(uPnpNamespaces.ud.GetName("depth"));
|
||||
var url = element.GetDescendantValue(uPnpNamespaces.ud.GetName("url"));
|
||||
var mimeType = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("mimetype"));
|
||||
var width = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("width"));
|
||||
var height = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("height"));
|
||||
var depth = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("depth"));
|
||||
var url = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("url"));
|
||||
|
||||
var widthValue = int.Parse(width, NumberStyles.Integer, UsCulture);
|
||||
var heightValue = int.Parse(height, NumberStyles.Integer, UsCulture);
|
||||
|
@ -1100,11 +1110,11 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private static DeviceService Create(XElement element)
|
||||
{
|
||||
var type = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceType"));
|
||||
var id = element.GetDescendantValue(uPnpNamespaces.ud.GetName("serviceId"));
|
||||
var scpdUrl = element.GetDescendantValue(uPnpNamespaces.ud.GetName("SCPDURL"));
|
||||
var controlURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("controlURL"));
|
||||
var eventSubURL = element.GetDescendantValue(uPnpNamespaces.ud.GetName("eventSubURL"));
|
||||
var type = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceType"));
|
||||
var id = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("serviceId"));
|
||||
var scpdUrl = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("SCPDURL"));
|
||||
var controlURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("controlURL"));
|
||||
var eventSubURL = element.GetDescendantValue(UPnpNamespaces.Ud.GetName("eventSubURL"));
|
||||
|
||||
return new DeviceService
|
||||
{
|
||||
|
@ -1116,14 +1126,7 @@ namespace Emby.Dlna.PlayTo
|
|||
};
|
||||
}
|
||||
|
||||
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
||||
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
|
||||
public event EventHandler<MediaChangedEventArgs> MediaChanged;
|
||||
|
||||
public uBaseObject CurrentMediaInfo { get; private set; }
|
||||
|
||||
private void UpdateMediaInfo(uBaseObject mediaInfo, TRANSPORTSTATE state)
|
||||
private void UpdateMediaInfo(UBaseObject mediaInfo, TransportState state)
|
||||
{
|
||||
TransportState = state;
|
||||
|
||||
|
@ -1132,7 +1135,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
if (previousMediaInfo == null && mediaInfo != null)
|
||||
{
|
||||
if (state != TRANSPORTSTATE.STOPPED)
|
||||
if (state != TransportState.Stopped)
|
||||
{
|
||||
OnPlaybackStart(mediaInfo);
|
||||
}
|
||||
|
@ -1151,7 +1154,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackStart(uBaseObject mediaInfo)
|
||||
private void OnPlaybackStart(UBaseObject mediaInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
|
||||
{
|
||||
|
@ -1164,7 +1167,7 @@ namespace Emby.Dlna.PlayTo
|
|||
});
|
||||
}
|
||||
|
||||
private void OnPlaybackProgress(uBaseObject mediaInfo)
|
||||
private void OnPlaybackProgress(UBaseObject mediaInfo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(mediaInfo.Url))
|
||||
{
|
||||
|
@ -1177,7 +1180,7 @@ namespace Emby.Dlna.PlayTo
|
|||
});
|
||||
}
|
||||
|
||||
private void OnPlaybackStop(uBaseObject mediaInfo)
|
||||
private void OnPlaybackStop(UBaseObject mediaInfo)
|
||||
{
|
||||
PlaybackStopped?.Invoke(this, new PlaybackStoppedEventArgs
|
||||
{
|
||||
|
@ -1185,7 +1188,7 @@ namespace Emby.Dlna.PlayTo
|
|||
});
|
||||
}
|
||||
|
||||
private void OnMediaChanged(uBaseObject old, uBaseObject newMedia)
|
||||
private void OnMediaChanged(UBaseObject old, UBaseObject newMedia)
|
||||
{
|
||||
MediaChanged?.Invoke(this, new MediaChangedEventArgs
|
||||
{
|
||||
|
@ -1194,14 +1197,17 @@ namespace Emby.Dlna.PlayTo
|
|||
});
|
||||
}
|
||||
|
||||
bool _disposed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
|
@ -1220,9 +1226,10 @@ namespace Emby.Dlna.PlayTo
|
|||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", Properties.Name, Properties.BaseUrl);
|
||||
return string.Format(CultureInfo.InvariantCulture, "{0} - {1}", Properties.Name, Properties.BaseUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,9 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class DeviceInfo
|
||||
{
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
private string _baseUrl = string.Empty;
|
||||
|
||||
public DeviceInfo()
|
||||
{
|
||||
Name = "Generic Device";
|
||||
|
@ -33,7 +36,6 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public string PresentationUrl { get; set; }
|
||||
|
||||
private string _baseUrl = string.Empty;
|
||||
public string BaseUrl
|
||||
{
|
||||
get => _baseUrl;
|
||||
|
@ -42,7 +44,6 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public DeviceIcon Icon { get; set; }
|
||||
|
||||
private readonly List<DeviceService> _services = new List<DeviceService>();
|
||||
public List<DeviceService> Services => _services;
|
||||
|
||||
public DeviceIdentification ToDeviceIdentification()
|
||||
|
|
13
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
Normal file
13
Emby.Dlna/PlayTo/MediaChangedEventArgs.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public UBaseObject OldMediaInfo { get; set; }
|
||||
|
||||
public UBaseObject NewMediaInfo { get; set; }
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -31,7 +31,6 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
private static readonly CultureInfo _usCulture = CultureInfo.ReadOnly(new CultureInfo("en-US"));
|
||||
|
||||
private Device _device;
|
||||
private readonly SessionInfo _session;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -50,6 +49,7 @@ namespace Emby.Dlna.PlayTo
|
|||
private readonly string _accessToken;
|
||||
|
||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||
private Device _device;
|
||||
private int _currentPlaylistIndex;
|
||||
|
||||
private bool _disposed;
|
||||
|
@ -372,8 +372,13 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
if (!command.ControllingUserId.Equals(Guid.Empty))
|
||||
{
|
||||
_sessionManager.LogSessionActivity(_session.Client, _session.ApplicationVersion, _session.DeviceId,
|
||||
_session.DeviceName, _session.RemoteEndPoint, user);
|
||||
_sessionManager.LogSessionActivity(
|
||||
_session.Client,
|
||||
_session.ApplicationVersion,
|
||||
_session.DeviceId,
|
||||
_session.DeviceName,
|
||||
_session.RemoteEndPoint,
|
||||
user);
|
||||
}
|
||||
|
||||
return PlayItems(playlist, cancellationToken);
|
||||
|
@ -498,42 +503,44 @@ namespace Emby.Dlna.PlayTo
|
|||
if (streamInfo.MediaType == DlnaProfileType.Audio)
|
||||
{
|
||||
return new ContentFeatureBuilder(profile)
|
||||
.BuildAudioHeader(streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
streamInfo.TargetAudioSampleRate,
|
||||
streamInfo.TargetAudioChannels,
|
||||
streamInfo.TargetAudioBitDepth,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TranscodeSeekInfo);
|
||||
.BuildAudioHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioBitrate,
|
||||
streamInfo.TargetAudioSampleRate,
|
||||
streamInfo.TargetAudioChannels,
|
||||
streamInfo.TargetAudioBitDepth,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TranscodeSeekInfo);
|
||||
}
|
||||
|
||||
if (streamInfo.MediaType == DlnaProfileType.Video)
|
||||
{
|
||||
var list = new ContentFeatureBuilder(profile)
|
||||
.BuildVideoHeader(streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoBitrate,
|
||||
streamInfo.TargetTimestamp,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
streamInfo.TranscodeSeekInfo,
|
||||
streamInfo.IsTargetAnamorphic,
|
||||
streamInfo.IsTargetInterlaced,
|
||||
streamInfo.TargetRefFrames,
|
||||
streamInfo.TargetVideoStreamCount,
|
||||
streamInfo.TargetAudioStreamCount,
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
.BuildVideoHeader(
|
||||
streamInfo.Container,
|
||||
streamInfo.TargetVideoCodec.FirstOrDefault(),
|
||||
streamInfo.TargetAudioCodec.FirstOrDefault(),
|
||||
streamInfo.TargetWidth,
|
||||
streamInfo.TargetHeight,
|
||||
streamInfo.TargetVideoBitDepth,
|
||||
streamInfo.TargetVideoBitrate,
|
||||
streamInfo.TargetTimestamp,
|
||||
streamInfo.IsDirectStream,
|
||||
streamInfo.RunTimeTicks ?? 0,
|
||||
streamInfo.TargetVideoProfile,
|
||||
streamInfo.TargetVideoLevel,
|
||||
streamInfo.TargetFramerate ?? 0,
|
||||
streamInfo.TargetPacketLength,
|
||||
streamInfo.TranscodeSeekInfo,
|
||||
streamInfo.IsTargetAnamorphic,
|
||||
streamInfo.IsTargetInterlaced,
|
||||
streamInfo.TargetRefFrames,
|
||||
streamInfo.TargetVideoStreamCount,
|
||||
streamInfo.TargetAudioStreamCount,
|
||||
streamInfo.TargetVideoCodecTag,
|
||||
streamInfo.IsTargetAVC);
|
||||
|
||||
return list.Count == 0 ? null : list[0];
|
||||
}
|
||||
|
@ -633,6 +640,10 @@ namespace Emby.Dlna.PlayTo
|
|||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
|
@ -673,48 +684,41 @@ namespace Emby.Dlna.PlayTo
|
|||
case GeneralCommandType.ToggleMute:
|
||||
return _device.ToggleMute(cancellationToken);
|
||||
case GeneralCommandType.SetAudioStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out string index))
|
||||
{
|
||||
if (command.Arguments.TryGetValue("Index", out string arg))
|
||||
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
|
||||
return SetAudioStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||
throw new ArgumentException("Unsupported SetAudioStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetAudioStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetSubtitleStreamIndex:
|
||||
if (command.Arguments.TryGetValue("Index", out index))
|
||||
{
|
||||
if (command.Arguments.TryGetValue("Index", out string arg))
|
||||
if (int.TryParse(index, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var val))
|
||||
{
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
|
||||
return SetSubtitleStreamIndex(val);
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
throw new ArgumentException("Unsupported SetSubtitleStreamIndex value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
|
||||
case GeneralCommandType.SetVolume:
|
||||
if (command.Arguments.TryGetValue("Volume", out string vol))
|
||||
{
|
||||
if (command.Arguments.TryGetValue("Volume", out string arg))
|
||||
if (int.TryParse(vol, NumberStyles.Integer, _usCulture, out var volume))
|
||||
{
|
||||
if (int.TryParse(arg, NumberStyles.Integer, _usCulture, out var volume))
|
||||
{
|
||||
return _device.SetVolume(volume, cancellationToken);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Unsupported volume value supplied.");
|
||||
return _device.SetVolume(volume, cancellationToken);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Volume argument cannot be null");
|
||||
throw new ArgumentException("Unsupported volume value supplied.");
|
||||
}
|
||||
|
||||
throw new ArgumentException("Volume argument cannot be null");
|
||||
default:
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -778,7 +782,7 @@ namespace Emby.Dlna.PlayTo
|
|||
const int maxWait = 15000000;
|
||||
const int interval = 500;
|
||||
var currentWait = 0;
|
||||
while (_device.TransportState != TRANSPORTSTATE.PLAYING && currentWait < maxWait)
|
||||
while (_device.TransportState != TransportState.Playing && currentWait < maxWait)
|
||||
{
|
||||
await Task.Delay(interval).ConfigureAwait(false);
|
||||
currentWait += interval;
|
||||
|
@ -787,8 +791,67 @@ namespace Emby.Dlna.PlayTo
|
|||
await _device.Seek(TimeSpan.FromTicks(positionTicks), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
if (_device == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||
}
|
||||
|
||||
// Not supported or needed right now
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private class StreamParams
|
||||
{
|
||||
private MediaSourceInfo mediaSource;
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
public bool IsDirectStream { get; set; }
|
||||
|
@ -809,15 +872,11 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
private MediaSourceInfo MediaSource;
|
||||
|
||||
private IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
|
||||
{
|
||||
if (MediaSource != null)
|
||||
if (mediaSource != null)
|
||||
{
|
||||
return MediaSource;
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
var hasMediaSources = Item as IHasMediaSources;
|
||||
|
@ -827,9 +886,9 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
|
||||
MediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
mediaSource = await _mediaSourceManager.GetMediaSource(Item, MediaSourceId, LiveStreamId, false, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return MediaSource;
|
||||
return mediaSource;
|
||||
}
|
||||
|
||||
private static Guid GetItemId(string url)
|
||||
|
@ -901,61 +960,5 @@ namespace Emby.Dlna.PlayTo
|
|||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
private static int? GetIntValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long GetLongValue(IReadOnlyDictionary<string, string> values, string name)
|
||||
{
|
||||
var value = values.GetValueOrDefault(name);
|
||||
|
||||
if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task SendMessage<T>(string name, Guid messageId, T data, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
|
||||
if (_device == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.Equals(name, "Play", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "PlayState", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||
}
|
||||
|
||||
if (string.Equals(name, "GeneralCommand", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SendGeneralCommand(data as GeneralCommand, cancellationToken);
|
||||
}
|
||||
|
||||
// Not supported or needed right now
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -92,7 +92,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
// It has to report that it's a media renderer
|
||||
if (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1 &&
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) == -1)
|
||||
{
|
||||
// _logger.LogDebug("Upnp device {0} does not contain a MediaRenderer device (0).", location);
|
||||
return;
|
||||
|
@ -174,7 +174,7 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
if (controller == null)
|
||||
{
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, cancellationToken).ConfigureAwait(false);
|
||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _logger, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
string deviceName = device.Properties.Name;
|
||||
|
||||
|
@ -192,20 +192,20 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
controller = new PlayToController(
|
||||
sessionInfo,
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_config,
|
||||
_mediaEncoder);
|
||||
_sessionManager,
|
||||
_libraryManager,
|
||||
_logger,
|
||||
_dlnaManager,
|
||||
_userManager,
|
||||
_imageProcessor,
|
||||
serverAddress,
|
||||
null,
|
||||
_deviceDiscovery,
|
||||
_userDataManager,
|
||||
_localization,
|
||||
_mediaSourceManager,
|
||||
_config,
|
||||
_mediaEncoder);
|
||||
|
||||
sessionInfo.AddController(controller);
|
||||
|
||||
|
@ -218,17 +218,17 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
PlayableMediaTypes = profile.GetSupportedMediaTypes(),
|
||||
|
||||
SupportedCommands = new string[]
|
||||
SupportedCommands = new[]
|
||||
{
|
||||
GeneralCommandType.VolumeDown.ToString(),
|
||||
GeneralCommandType.VolumeUp.ToString(),
|
||||
GeneralCommandType.Mute.ToString(),
|
||||
GeneralCommandType.Unmute.ToString(),
|
||||
GeneralCommandType.ToggleMute.ToString(),
|
||||
GeneralCommandType.SetVolume.ToString(),
|
||||
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
|
||||
GeneralCommandType.PlayMediaSource.ToString()
|
||||
GeneralCommandType.VolumeDown.ToString(),
|
||||
GeneralCommandType.VolumeUp.ToString(),
|
||||
GeneralCommandType.Mute.ToString(),
|
||||
GeneralCommandType.Unmute.ToString(),
|
||||
GeneralCommandType.ToggleMute.ToString(),
|
||||
GeneralCommandType.SetVolume.ToString(),
|
||||
GeneralCommandType.SetAudioStreamIndex.ToString(),
|
||||
GeneralCommandType.SetSubtitleStreamIndex.ToString(),
|
||||
GeneralCommandType.PlayMediaSource.ToString()
|
||||
},
|
||||
|
||||
SupportsMediaControl = true
|
||||
|
@ -247,8 +247,9 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
_disposeCancellationTokenSource.Cancel();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Error while disposing PlayToManager");
|
||||
}
|
||||
|
||||
_sessionLock.Dispose();
|
||||
|
|
|
@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class PlaybackProgressEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class PlaybackStartEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,6 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class PlaybackStoppedEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
|
||||
public class MediaChangedEventArgs : EventArgs
|
||||
{
|
||||
public uBaseObject OldMediaInfo { get; set; }
|
||||
|
||||
public uBaseObject NewMediaInfo { get; set; }
|
||||
public UBaseObject MediaInfo { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public enum TRANSPORTSTATE
|
||||
{
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
TRANSITIONING,
|
||||
PAUSED_PLAYBACK,
|
||||
PAUSED
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Emby.Dlna.Common;
|
||||
|
@ -11,36 +12,30 @@ namespace Emby.Dlna.PlayTo
|
|||
{
|
||||
public class TransportCommands
|
||||
{
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
private List<StateVariable> _stateVariables = new List<StateVariable>();
|
||||
public List<StateVariable> StateVariables
|
||||
{
|
||||
get => _stateVariables;
|
||||
set => _stateVariables = value;
|
||||
}
|
||||
|
||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
||||
public List<ServiceAction> ServiceActions
|
||||
{
|
||||
get => _serviceActions;
|
||||
set => _serviceActions = value;
|
||||
}
|
||||
|
||||
public List<StateVariable> StateVariables => _stateVariables;
|
||||
|
||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
||||
|
||||
public static TransportCommands Create(XDocument document)
|
||||
{
|
||||
var command = new TransportCommands();
|
||||
|
||||
var actionList = document.Descendants(uPnpNamespaces.svc + "actionList");
|
||||
var actionList = document.Descendants(UPnpNamespaces.Svc + "actionList");
|
||||
|
||||
foreach (var container in actionList.Descendants(uPnpNamespaces.svc + "action"))
|
||||
foreach (var container in actionList.Descendants(UPnpNamespaces.Svc + "action"))
|
||||
{
|
||||
command.ServiceActions.Add(ServiceActionFromXml(container));
|
||||
}
|
||||
|
||||
var stateValues = document.Descendants(uPnpNamespaces.ServiceStateTable).FirstOrDefault();
|
||||
var stateValues = document.Descendants(UPnpNamespaces.ServiceStateTable).FirstOrDefault();
|
||||
|
||||
if (stateValues != null)
|
||||
{
|
||||
foreach (var container in stateValues.Elements(uPnpNamespaces.svc + "stateVariable"))
|
||||
foreach (var container in stateValues.Elements(UPnpNamespaces.Svc + "stateVariable"))
|
||||
{
|
||||
command.StateVariables.Add(FromXml(container));
|
||||
}
|
||||
|
@ -51,19 +46,19 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
private static ServiceAction ServiceActionFromXml(XElement container)
|
||||
{
|
||||
var argumentList = new List<Argument>();
|
||||
var serviceAction = new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
};
|
||||
|
||||
foreach (var arg in container.Descendants(uPnpNamespaces.svc + "argument"))
|
||||
var argumentList = serviceAction.ArgumentList;
|
||||
|
||||
foreach (var arg in container.Descendants(UPnpNamespaces.Svc + "argument"))
|
||||
{
|
||||
argumentList.Add(ArgumentFromXml(arg));
|
||||
}
|
||||
|
||||
return new ServiceAction
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
|
||||
ArgumentList = argumentList
|
||||
};
|
||||
return serviceAction;
|
||||
}
|
||||
|
||||
private static Argument ArgumentFromXml(XElement container)
|
||||
|
@ -75,29 +70,29 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
return new Argument
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
Direction = container.GetValue(uPnpNamespaces.svc + "direction"),
|
||||
RelatedStateVariable = container.GetValue(uPnpNamespaces.svc + "relatedStateVariable")
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
Direction = container.GetValue(UPnpNamespaces.Svc + "direction"),
|
||||
RelatedStateVariable = container.GetValue(UPnpNamespaces.Svc + "relatedStateVariable")
|
||||
};
|
||||
}
|
||||
|
||||
private static StateVariable FromXml(XElement container)
|
||||
{
|
||||
var allowedValues = new List<string>();
|
||||
var element = container.Descendants(uPnpNamespaces.svc + "allowedValueList")
|
||||
var element = container.Descendants(UPnpNamespaces.Svc + "allowedValueList")
|
||||
.FirstOrDefault();
|
||||
|
||||
if (element != null)
|
||||
{
|
||||
var values = element.Descendants(uPnpNamespaces.svc + "allowedValue");
|
||||
var values = element.Descendants(UPnpNamespaces.Svc + "allowedValue");
|
||||
|
||||
allowedValues.AddRange(values.Select(child => child.Value));
|
||||
}
|
||||
|
||||
return new StateVariable
|
||||
{
|
||||
Name = container.GetValue(uPnpNamespaces.svc + "name"),
|
||||
DataType = container.GetValue(uPnpNamespaces.svc + "dataType"),
|
||||
Name = container.GetValue(UPnpNamespaces.Svc + "name"),
|
||||
DataType = container.GetValue(UPnpNamespaces.Svc + "dataType"),
|
||||
AllowedValues = allowedValues.ToArray()
|
||||
};
|
||||
}
|
||||
|
@ -123,7 +118,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamespace, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
|
||||
|
@ -147,7 +142,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
}
|
||||
|
||||
public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
|
||||
|
@ -170,7 +165,7 @@ namespace Emby.Dlna.PlayTo
|
|||
}
|
||||
}
|
||||
|
||||
return string.Format(CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
|
||||
}
|
||||
|
||||
private string BuildArgumentXml(Argument argument, string value, string commandParameter = "")
|
||||
|
@ -180,15 +175,12 @@ namespace Emby.Dlna.PlayTo
|
|||
if (state != null)
|
||||
{
|
||||
var sendValue = state.AllowedValues.FirstOrDefault(a => string.Equals(a, commandParameter, StringComparison.OrdinalIgnoreCase)) ??
|
||||
state.AllowedValues.FirstOrDefault() ??
|
||||
value;
|
||||
(state.AllowedValues.Count > 0 ? state.AllowedValues[0] : value);
|
||||
|
||||
return string.Format("<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0} xmlns:dt=\"urn:schemas-microsoft-com:datatypes\" dt:dt=\"{1}\">{2}</{0}>", argument.Name, state.DataType ?? "string", sendValue);
|
||||
}
|
||||
|
||||
return string.Format("<{0}>{1}</{0}>", argument.Name, value);
|
||||
return string.Format(CultureInfo.InvariantCulture, "<{0}>{1}</{0}>", argument.Name, value);
|
||||
}
|
||||
|
||||
private const string CommandBase = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n" + "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + "<SOAP-ENV:Body>" + "<m:{0} xmlns:m=\"{1}\">" + "{2}" + "</m:{0}>" + "</SOAP-ENV:Body></SOAP-ENV:Envelope>";
|
||||
}
|
||||
}
|
||||
|
|
14
Emby.Dlna/PlayTo/TransportState.cs
Normal file
14
Emby.Dlna/PlayTo/TransportState.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable SA1602
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public enum TransportState
|
||||
{
|
||||
Stopped,
|
||||
Playing,
|
||||
Transitioning,
|
||||
PausedPlayback,
|
||||
Paused
|
||||
}
|
||||
}
|
|
@ -6,22 +6,22 @@ using Emby.Dlna.Ssdp;
|
|||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class UpnpContainer : uBaseObject
|
||||
public class UpnpContainer : UBaseObject
|
||||
{
|
||||
public static uBaseObject Create(XElement container)
|
||||
public static UBaseObject Create(XElement container)
|
||||
{
|
||||
if (container == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(container));
|
||||
}
|
||||
|
||||
return new uBaseObject
|
||||
return new UBaseObject
|
||||
{
|
||||
Id = container.GetAttributeValue(uPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(uPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(uPnpNamespaces.title),
|
||||
IconUrl = container.GetValue(uPnpNamespaces.Artwork),
|
||||
UpnpClass = container.GetValue(uPnpNamespaces.uClass)
|
||||
Id = container.GetAttributeValue(UPnpNamespaces.Id),
|
||||
ParentId = container.GetAttributeValue(UPnpNamespaces.ParentId),
|
||||
Title = container.GetValue(UPnpNamespaces.Title),
|
||||
IconUrl = container.GetValue(UPnpNamespaces.Artwork),
|
||||
UpnpClass = container.GetValue(UPnpNamespaces.Class)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class uBaseObject
|
||||
public class UBaseObject
|
||||
{
|
||||
public string Id { get; set; }
|
||||
|
||||
|
@ -20,20 +21,10 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
public string Url { get; set; }
|
||||
|
||||
public string[] ProtocolInfo { get; set; }
|
||||
public IReadOnlyList<string> ProtocolInfo { get; set; }
|
||||
|
||||
public string UpnpClass { get; set; }
|
||||
|
||||
public bool Equals(uBaseObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
return string.Equals(Id, obj.Id);
|
||||
}
|
||||
|
||||
public string MediaType
|
||||
{
|
||||
get
|
||||
|
@ -58,5 +49,15 @@ namespace Emby.Dlna.PlayTo
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Equals(UBaseObject obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(obj));
|
||||
}
|
||||
|
||||
return string.Equals(Id, obj.Id, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,38 +4,64 @@ using System.Xml.Linq;
|
|||
|
||||
namespace Emby.Dlna.PlayTo
|
||||
{
|
||||
public class uPnpNamespaces
|
||||
public static class UPnpNamespaces
|
||||
{
|
||||
public static XNamespace dc = "http://purl.org/dc/elements/1.1/";
|
||||
public static XNamespace ns = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
public static XNamespace svc = "urn:schemas-upnp-org:service-1-0";
|
||||
public static XNamespace ud = "urn:schemas-upnp-org:device-1-0";
|
||||
public static XNamespace upnp = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
public static XNamespace RenderingControl = "urn:schemas-upnp-org:service:RenderingControl:1";
|
||||
public static XNamespace AvTransport = "urn:schemas-upnp-org:service:AVTransport:1";
|
||||
public static XNamespace ContentDirectory = "urn:schemas-upnp-org:service:ContentDirectory:1";
|
||||
public static XNamespace Dc { get; } = "http://purl.org/dc/elements/1.1/";
|
||||
|
||||
public static XName containers = ns + "container";
|
||||
public static XName items = ns + "item";
|
||||
public static XName title = dc + "title";
|
||||
public static XName creator = dc + "creator";
|
||||
public static XName artist = upnp + "artist";
|
||||
public static XName Id = "id";
|
||||
public static XName ParentId = "parentID";
|
||||
public static XName uClass = upnp + "class";
|
||||
public static XName Artwork = upnp + "albumArtURI";
|
||||
public static XName Description = dc + "description";
|
||||
public static XName LongDescription = upnp + "longDescription";
|
||||
public static XName Album = upnp + "album";
|
||||
public static XName Author = upnp + "author";
|
||||
public static XName Director = upnp + "director";
|
||||
public static XName PlayCount = upnp + "playbackCount";
|
||||
public static XName Tracknumber = upnp + "originalTrackNumber";
|
||||
public static XName Res = ns + "res";
|
||||
public static XName Duration = "duration";
|
||||
public static XName ProtocolInfo = "protocolInfo";
|
||||
public static XNamespace Ns { get; } = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/";
|
||||
|
||||
public static XName ServiceStateTable = svc + "serviceStateTable";
|
||||
public static XName StateVariable = svc + "stateVariable";
|
||||
public static XNamespace Svc { get; } = "urn:schemas-upnp-org:service-1-0";
|
||||
|
||||
public static XNamespace Ud { get; } = "urn:schemas-upnp-org:device-1-0";
|
||||
|
||||
public static XNamespace UPnp { get; } = "urn:schemas-upnp-org:metadata-1-0/upnp/";
|
||||
|
||||
public static XNamespace RenderingControl { get; } = "urn:schemas-upnp-org:service:RenderingControl:1";
|
||||
|
||||
public static XNamespace AvTransport { get; } = "urn:schemas-upnp-org:service:AVTransport:1";
|
||||
|
||||
public static XNamespace ContentDirectory { get; } = "urn:schemas-upnp-org:service:ContentDirectory:1";
|
||||
|
||||
public static XName Containers { get; } = Ns + "container";
|
||||
|
||||
public static XName Items { get; } = Ns + "item";
|
||||
|
||||
public static XName Title { get; } = Dc + "title";
|
||||
|
||||
public static XName Creator { get; } = Dc + "creator";
|
||||
|
||||
public static XName Artist { get; } = UPnp + "artist";
|
||||
|
||||
public static XName Id { get; } = "id";
|
||||
|
||||
public static XName ParentId { get; } = "parentID";
|
||||
|
||||
public static XName Class { get; } = UPnp + "class";
|
||||
|
||||
public static XName Artwork { get; } = UPnp + "albumArtURI";
|
||||
|
||||
public static XName Description { get; } = Dc + "description";
|
||||
|
||||
public static XName LongDescription { get; } = UPnp + "longDescription";
|
||||
|
||||
public static XName Album { get; } = UPnp + "album";
|
||||
|
||||
public static XName Author { get; } = UPnp + "author";
|
||||
|
||||
public static XName Director { get; } = UPnp + "director";
|
||||
|
||||
public static XName PlayCount { get; } = UPnp + "playbackCount";
|
||||
|
||||
public static XName Tracknumber { get; } = UPnp + "originalTrackNumber";
|
||||
|
||||
public static XName Res { get; } = Ns + "res";
|
||||
|
||||
public static XName Duration { get; } = "duration";
|
||||
|
||||
public static XName ProtocolInfo { get; } = "protocolInfo";
|
||||
|
||||
public static XName ServiceStateTable { get; } = Svc + "serviceStateTable";
|
||||
|
||||
public static XName StateVariable { get; } = Svc + "stateVariable";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,14 +64,14 @@ namespace Emby.Dlna.Profiles
|
|||
new DirectPlayProfile
|
||||
{
|
||||
// play all
|
||||
Container = "",
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new DirectPlayProfile
|
||||
{
|
||||
// play all
|
||||
Container = "",
|
||||
Container = string.Empty,
|
||||
Type = DlnaProfileType.Audio
|
||||
}
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Match = HeaderMatchType.Substring,
|
||||
Name = "User-Agent",
|
||||
Value ="Zip_"
|
||||
Value = "Zip_"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -81,7 +81,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -124,7 +124,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -161,7 +161,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3,he-aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -177,7 +177,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -192,7 +192,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
// The device does not have any audio switching capabilities
|
||||
new ProfileCondition
|
||||
|
|
|
@ -84,7 +84,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[]
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace Emby.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[]
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
|
|
|
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -93,8 +93,8 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Codec="h264",
|
||||
Conditions = new []
|
||||
Codec = "h264",
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition(ProfileConditionType.EqualsAny, ProfileConditionValue.VideoProfile, "baseline|constrained baseline"),
|
||||
new ProfileCondition
|
||||
|
@ -122,7 +122,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Audio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -182,7 +182,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Audio,
|
||||
Codec = "mp3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -202,7 +202,7 @@ namespace Emby.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
ResponseProfiles = new ResponseProfile[]
|
||||
ResponseProfiles = new[]
|
||||
{
|
||||
new ResponseProfile
|
||||
{
|
||||
|
|
|
@ -139,7 +139,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -150,7 +150,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -197,7 +197,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -166,7 +166,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -185,7 +185,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -114,7 +114,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -156,7 +156,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -172,7 +172,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -191,7 +191,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
|
|||
VideoCodec = "h264,mpeg4,vc1",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
|
|
|
@ -102,13 +102,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -128,13 +128,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -148,28 +148,28 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="mpeg2video",
|
||||
VideoCodec = "mpeg2video",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec="mpeg1video,mpeg2video",
|
||||
VideoCodec = "mpeg1video,mpeg2video",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
Type = DlnaProfileType.Video
|
||||
}
|
||||
};
|
||||
|
@ -180,7 +180,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -204,7 +204,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -243,7 +243,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "mpeg2video",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -275,7 +275,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -303,7 +303,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -319,7 +319,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -341,7 +341,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "mp3,mp2",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -120,7 +120,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -143,13 +143,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -169,13 +169,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -189,28 +189,28 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="mpeg2video",
|
||||
VideoCodec = "mpeg2video",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec="mpeg1video,mpeg2video",
|
||||
VideoCodec = "mpeg1video,mpeg2video",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new ResponseProfile
|
||||
|
@ -227,7 +227,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -266,7 +266,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "mpeg2video",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -298,7 +298,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -326,7 +326,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -364,7 +364,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "mp3,mp2",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -131,13 +131,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -157,13 +157,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -177,28 +177,28 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="mpeg2video",
|
||||
VideoCodec = "mpeg2video",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec="mpeg1video,mpeg2video",
|
||||
VideoCodec = "mpeg1video,mpeg2video",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new ResponseProfile
|
||||
|
@ -215,7 +215,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -282,7 +282,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "mp3,mp2",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="mpeg2video",
|
||||
VideoCodec = "mpeg2video",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec="mpeg1video,mpeg2video",
|
||||
VideoCodec = "mpeg1video,mpeg2video",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new ResponseProfile
|
||||
|
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
CodecProfiles = new[]
|
||||
{
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "mp3,mp2",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -187,13 +187,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_T,AVC_TS_HD_50_AC3_T,AVC_TS_HD_60_AC3_T,AVC_TS_HD_EU_T",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -213,13 +213,13 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
OrgPn = "AVC_TS_HD_24_AC3_ISO,AVC_TS_HD_50_AC3_ISO,AVC_TS_HD_60_AC3_ISO,AVC_TS_HD_EU_ISO",
|
||||
Type = DlnaProfileType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -233,28 +233,28 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="h264",
|
||||
AudioCodec="ac3,aac,mp3",
|
||||
VideoCodec = "h264",
|
||||
AudioCodec = "ac3,aac,mp3",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
OrgPn = "AVC_TS_HD_24_AC3,AVC_TS_HD_50_AC3,AVC_TS_HD_60_AC3,AVC_TS_HD_EU",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "ts,mpegts",
|
||||
VideoCodec="mpeg2video",
|
||||
VideoCodec = "mpeg2video",
|
||||
MimeType = "video/vnd.dlna.mpeg-tts",
|
||||
OrgPn="MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
OrgPn = "MPEG_TS_SD_EU,MPEG_TS_SD_NA,MPEG_TS_SD_KO",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
new ResponseProfile
|
||||
{
|
||||
Container = "mpeg",
|
||||
VideoCodec="mpeg1video,mpeg2video",
|
||||
VideoCodec = "mpeg1video,mpeg2video",
|
||||
MimeType = "video/mpeg",
|
||||
OrgPn="MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
OrgPn = "MPEG_PS_NTSC,MPEG_PS_PAL",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
new ResponseProfile
|
||||
|
@ -265,14 +265,13 @@ namespace Emby.Dlna.Profiles
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
CodecProfiles = new[]
|
||||
{
|
||||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -300,7 +299,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "mp3,mp2",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -108,7 +108,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -133,7 +133,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -176,7 +176,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -201,7 +201,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "wmapro",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -217,7 +217,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -235,7 +235,7 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "mp4,mov",
|
||||
AudioCodec="aac",
|
||||
AudioCodec = "aac",
|
||||
MimeType = "video/mp4",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
@ -244,7 +244,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Container = "avi",
|
||||
MimeType = "video/divx",
|
||||
OrgPn="AVI",
|
||||
OrgPn = "AVI",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -135,7 +135,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -178,7 +178,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -203,7 +203,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "wmapro",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -219,7 +219,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -237,7 +237,7 @@ namespace Emby.Dlna.Profiles
|
|||
new ResponseProfile
|
||||
{
|
||||
Container = "mp4,mov",
|
||||
AudioCodec="aac",
|
||||
AudioCodec = "aac",
|
||||
MimeType = "video/mp4",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
@ -246,7 +246,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Container = "avi",
|
||||
MimeType = "video/divx",
|
||||
OrgPn="AVI",
|
||||
OrgPn = "AVI",
|
||||
Type = DlnaProfileType.Video
|
||||
},
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace Emby.Dlna.Profiles
|
|||
|
||||
Headers = new[]
|
||||
{
|
||||
new HttpHeaderInfo {Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring},
|
||||
new HttpHeaderInfo { Name = "User-Agent", Value = "alphanetworks", Match = HeaderMatchType.Substring },
|
||||
new HttpHeaderInfo
|
||||
{
|
||||
Name = "User-Agent",
|
||||
|
@ -168,7 +168,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = DlnaProfileType.Photo,
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -193,7 +193,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -221,7 +221,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -119,7 +119,7 @@ namespace Emby.Dlna.Profiles
|
|||
Type = DlnaProfileType.Video,
|
||||
Container = "mp4,mov",
|
||||
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -138,7 +138,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "mpeg4",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -187,7 +187,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "h264",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -236,7 +236,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.Video,
|
||||
Codec = "wmv2,wmv3,vc1",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -284,7 +284,7 @@ namespace Emby.Dlna.Profiles
|
|||
new CodecProfile
|
||||
{
|
||||
Type = CodecType.Video,
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -307,7 +307,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "ac3,wmav2,wmapro",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
@ -323,7 +323,7 @@ namespace Emby.Dlna.Profiles
|
|||
{
|
||||
Type = CodecType.VideoAudio,
|
||||
Codec = "aac",
|
||||
Conditions = new []
|
||||
Conditions = new[]
|
||||
{
|
||||
new ProfileCondition
|
||||
{
|
||||
|
|
|
@ -15,11 +15,7 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
public abstract class BaseControlHandler
|
||||
{
|
||||
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
protected IServerConfigurationManager Config { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
protected BaseControlHandler(IServerConfigurationManager config, ILogger logger)
|
||||
{
|
||||
|
@ -27,6 +23,10 @@ namespace Emby.Dlna.Service
|
|||
Logger = logger;
|
||||
}
|
||||
|
||||
protected IServerConfigurationManager Config { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
public async Task<ControlResponse> ProcessControlRequestAsync(ControlRequest request)
|
||||
{
|
||||
try
|
||||
|
@ -80,10 +80,10 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
writer.WriteStartDocument(true);
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
|
||||
writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI);
|
||||
|
||||
WriteResult(requestInfo.LocalName, requestInfo.Headers, writer);
|
||||
|
@ -210,15 +210,6 @@ namespace Emby.Dlna.Service
|
|||
}
|
||||
}
|
||||
|
||||
private class ControlRequestInfo
|
||||
{
|
||||
public string LocalName { get; set; }
|
||||
|
||||
public string NamespaceURI { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
protected abstract void WriteResult(string methodName, IDictionary<string, string> methodParams, XmlWriter xmlWriter);
|
||||
|
||||
private void LogRequest(ControlRequest request)
|
||||
|
@ -240,5 +231,14 @@ namespace Emby.Dlna.Service
|
|||
|
||||
Logger.LogDebug("Control response. Headers: {@Headers}\n{Xml}", response.Headers, response.Xml);
|
||||
}
|
||||
|
||||
private class ControlRequestInfo
|
||||
{
|
||||
public string LocalName { get; set; }
|
||||
|
||||
public string NamespaceURI { get; set; }
|
||||
|
||||
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,20 +6,22 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace Emby.Dlna.Service
|
||||
{
|
||||
public class BaseService : IEventManager
|
||||
public class BaseService : IDlnaEventManager
|
||||
{
|
||||
protected IEventManager EventManager;
|
||||
protected IHttpClient HttpClient;
|
||||
protected ILogger Logger;
|
||||
|
||||
protected BaseService(ILogger<BaseService> logger, IHttpClient httpClient)
|
||||
{
|
||||
Logger = logger;
|
||||
HttpClient = httpClient;
|
||||
|
||||
EventManager = new EventManager(logger, HttpClient);
|
||||
EventManager = new DlnaEventManager(logger, HttpClient);
|
||||
}
|
||||
|
||||
protected IDlnaEventManager EventManager { get; }
|
||||
|
||||
protected IHttpClient HttpClient { get; }
|
||||
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
public EventSubscriptionResponse CancelEventSubscription(string subscriptionId)
|
||||
{
|
||||
return EventManager.CancelEventSubscription(subscriptionId);
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
public static class ControlErrorHandler
|
||||
{
|
||||
private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
private const string NsSoapEnv = "http://schemas.xmlsoap.org/soap/envelope/";
|
||||
|
||||
public static ControlResponse GetResponse(Exception ex)
|
||||
{
|
||||
|
@ -26,11 +26,11 @@ namespace Emby.Dlna.Service
|
|||
{
|
||||
writer.WriteStartDocument(true);
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
writer.WriteStartElement("SOAP-ENV", "Envelope", NsSoapEnv);
|
||||
writer.WriteAttributeString(string.Empty, "encodingStyle", NsSoapEnv, "http://schemas.xmlsoap.org/soap/encoding/");
|
||||
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV);
|
||||
writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV);
|
||||
writer.WriteStartElement("SOAP-ENV", "Body", NsSoapEnv);
|
||||
writer.WriteStartElement("SOAP-ENV", "Fault", NsSoapEnv);
|
||||
|
||||
writer.WriteElementString("faultcode", "500");
|
||||
writer.WriteElementString("faultstring", ex.Message);
|
||||
|
|
|
@ -87,7 +87,7 @@ namespace Emby.Dlna.Service
|
|||
.Append(SecurityElement.Escape(item.DataType ?? string.Empty))
|
||||
.Append("</dataType>");
|
||||
|
||||
if (item.AllowedValues.Length > 0)
|
||||
if (item.AllowedValues.Count > 0)
|
||||
{
|
||||
builder.Append("<allowedValueList>");
|
||||
foreach (var allowedValue in item.AllowedValues)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -17,9 +17,17 @@ namespace Emby.Dlna.Ssdp
|
|||
|
||||
private readonly IServerConfigurationManager _config;
|
||||
|
||||
private SsdpDeviceLocator _deviceLocator;
|
||||
private ISsdpCommunicationsServer _commsServer;
|
||||
|
||||
private int _listenerCount;
|
||||
private bool _disposed;
|
||||
|
||||
public DeviceDiscovery(IServerConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
private event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscoveredInternal;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -49,15 +57,6 @@ namespace Emby.Dlna.Ssdp
|
|||
/// <inheritdoc />
|
||||
public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft;
|
||||
|
||||
private SsdpDeviceLocator _deviceLocator;
|
||||
|
||||
private ISsdpCommunicationsServer _commsServer;
|
||||
|
||||
public DeviceDiscovery(IServerConfigurationManager config)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
// Call this method from somewhere in your code to start the search.
|
||||
public void Start(ISsdpCommunicationsServer communicationsServer)
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ using System.Xml.Linq;
|
|||
|
||||
namespace Emby.Dlna.Ssdp
|
||||
{
|
||||
public static class Extensions
|
||||
public static class SsdpExtensions
|
||||
{
|
||||
public static string GetValue(this XElement container, XName name)
|
||||
{
|
|
@ -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,145 +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.AddSingleton<TranscodingJobHelper>();
|
||||
ServiceCollection.AddScoped<MediaInfoHelper>();
|
||||
ServiceCollection.AddScoped<AudioHelper>();
|
||||
ServiceCollection.AddScoped<DynamicHlsHelper>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -645,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();
|
||||
|
@ -747,7 +752,6 @@ namespace Emby.Server.Implementations
|
|||
CollectionFolder.XmlSerializer = _xmlSerializer;
|
||||
CollectionFolder.JsonSerializer = Resolve<IJsonSerializer>();
|
||||
CollectionFolder.ApplicationHost = this;
|
||||
AuthenticatedAttribute.AuthService = Resolve<IAuthService>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -767,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>(),
|
||||
|
@ -831,6 +836,8 @@ namespace Emby.Server.Implementations
|
|||
{
|
||||
hasPluginConfiguration.SetStartupInfo(s => Directory.CreateDirectory(s));
|
||||
}
|
||||
|
||||
plugin.RegisterServices(ServiceCollection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -931,7 +938,7 @@ namespace Emby.Server.Implementations
|
|||
}
|
||||
}
|
||||
|
||||
if (!_httpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
|
||||
if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
requiresRestart = true;
|
||||
}
|
||||
|
@ -1385,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)
|
||||
|
|
|
@ -746,12 +746,21 @@ namespace Emby.Server.Implementations.Channels
|
|||
// null if came from cache
|
||||
if (itemsResult != null)
|
||||
{
|
||||
var internalItems = itemsResult.Items
|
||||
.Select(i => GetChannelItemEntity(i, channelProvider, channel.Id, parentItem, cancellationToken))
|
||||
.ToArray();
|
||||
var items = itemsResult.Items;
|
||||
var itemsLen = items.Count;
|
||||
var internalItems = new Guid[itemsLen];
|
||||
for (int i = 0; i < itemsLen; i++)
|
||||
{
|
||||
internalItems[i] = (await GetChannelItemEntityAsync(
|
||||
items[i],
|
||||
channelProvider,
|
||||
channel.Id,
|
||||
parentItem,
|
||||
cancellationToken).ConfigureAwait(false)).Id;
|
||||
}
|
||||
|
||||
var existingIds = _libraryManager.GetItemIds(query);
|
||||
var deadIds = existingIds.Except(internalItems.Select(i => i.Id))
|
||||
var deadIds = existingIds.Except(internalItems)
|
||||
.ToArray();
|
||||
|
||||
foreach (var deadId in deadIds)
|
||||
|
@ -963,7 +972,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
return item;
|
||||
}
|
||||
|
||||
private BaseItem GetChannelItemEntity(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
|
||||
private async Task<BaseItem> GetChannelItemEntityAsync(ChannelItemInfo info, IChannel channelProvider, Guid internalChannelId, BaseItem parentFolder, CancellationToken cancellationToken)
|
||||
{
|
||||
var parentFolderId = parentFolder.Id;
|
||||
|
||||
|
@ -1165,7 +1174,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
}
|
||||
else if (forceUpdate)
|
||||
{
|
||||
item.UpdateToRepository(ItemUpdateType.None, cancellationToken);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.None, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if ((isNew || forceUpdate) && info.Type == ChannelItemType.Media)
|
||||
|
|
|
@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public BoxSet CreateCollection(CollectionCreationOptions options)
|
||||
public async Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options)
|
||||
{
|
||||
var name = options.Name;
|
||||
|
||||
|
@ -141,7 +141,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
// This could cause it to get re-resolved as a plain folder
|
||||
var folderName = _fileSystem.GetValidFilename(name) + " [boxset]";
|
||||
|
||||
var parentFolder = GetCollectionsFolder(true).GetAwaiter().GetResult();
|
||||
var parentFolder = await GetCollectionsFolder(true).ConfigureAwait(false);
|
||||
|
||||
if (parentFolder == null)
|
||||
{
|
||||
|
@ -169,12 +169,16 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
if (options.ItemIdList.Length > 0)
|
||||
{
|
||||
AddToCollection(collection.Id, options.ItemIdList, false, new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
// The initial adding of items is going to create a local metadata file
|
||||
// This will cause internet metadata to be skipped as a result
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
|
||||
});
|
||||
await AddToCollectionAsync(
|
||||
collection.Id,
|
||||
options.ItemIdList.Select(x => new Guid(x)),
|
||||
false,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
{
|
||||
// The initial adding of items is going to create a local metadata file
|
||||
// This will cause internet metadata to be skipped as a result
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -197,18 +201,10 @@ namespace Emby.Server.Implementations.Collections
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddToCollection(Guid collectionId, IEnumerable<string> ids)
|
||||
{
|
||||
AddToCollection(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||
}
|
||||
public Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids)
|
||||
=> AddToCollectionAsync(collectionId, ids, true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddToCollection(Guid collectionId, IEnumerable<Guid> ids)
|
||||
{
|
||||
AddToCollection(collectionId, ids.Select(i => i.ToString("N", CultureInfo.InvariantCulture)), true, new MetadataRefreshOptions(new DirectoryService(_fileSystem)));
|
||||
}
|
||||
|
||||
private void AddToCollection(Guid collectionId, IEnumerable<string> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
||||
private async Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> ids, bool fireEvent, MetadataRefreshOptions refreshOptions)
|
||||
{
|
||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
||||
if (collection == null)
|
||||
|
@ -224,15 +220,14 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
var guidId = new Guid(id);
|
||||
var item = _libraryManager.GetItemById(guidId);
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
|
||||
if (item == null)
|
||||
{
|
||||
throw new ArgumentException("No item exists with the supplied Id");
|
||||
}
|
||||
|
||||
if (!currentLinkedChildrenIds.Contains(guidId))
|
||||
if (!currentLinkedChildrenIds.Contains(id))
|
||||
{
|
||||
itemList.Add(item);
|
||||
|
||||
|
@ -249,7 +244,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
|
||||
collection.UpdateRatingToItems(linkedChildrenList);
|
||||
|
||||
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
|
||||
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
refreshOptions.ForceSave = true;
|
||||
_providerManager.QueueRefresh(collection.Id, refreshOptions, RefreshPriority.High);
|
||||
|
@ -266,13 +261,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveFromCollection(Guid collectionId, IEnumerable<string> itemIds)
|
||||
{
|
||||
RemoveFromCollection(collectionId, itemIds.Select(i => new Guid(i)));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveFromCollection(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||
public async Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds)
|
||||
{
|
||||
var collection = _libraryManager.GetItemById(collectionId) as BoxSet;
|
||||
|
||||
|
@ -309,7 +298,7 @@ namespace Emby.Server.Implementations.Collections
|
|||
collection.LinkedChildren = collection.LinkedChildren.Except(list).ToArray();
|
||||
}
|
||||
|
||||
collection.UpdateToRepository(ItemUpdateType.MetadataEdit, CancellationToken.None);
|
||||
await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(
|
||||
collection.Id,
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -4308,7 +4308,7 @@ namespace Emby.Server.Implementations.Data
|
|||
whereClauses.Add("ProductionYear=@Years");
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@Years", query.Years[0].ToString());
|
||||
statement.TryBind("@Years", query.Years[0].ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
else if (query.Years.Length > 1)
|
||||
|
@ -4560,13 +4560,13 @@ namespace Emby.Server.Implementations.Data
|
|||
if (query.AncestorIds.Length > 1)
|
||||
{
|
||||
var inClause = string.Join(",", query.AncestorIds.Select(i => "'" + i.ToString("N", CultureInfo.InvariantCulture) + "'"));
|
||||
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
|
||||
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorIdText in ({0}))", inClause));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey))
|
||||
{
|
||||
var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey";
|
||||
whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
|
||||
whereClauses.Add(string.Format(CultureInfo.InvariantCulture, "Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause));
|
||||
if (statement != null)
|
||||
{
|
||||
statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey);
|
||||
|
@ -5170,7 +5170,10 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
insertText.Append(',');
|
||||
}
|
||||
|
||||
insertText.AppendFormat("(@ItemId, @AncestorId{0}, @AncestorIdText{0})", i.ToString(CultureInfo.InvariantCulture));
|
||||
insertText.AppendFormat(
|
||||
CultureInfo.InvariantCulture,
|
||||
"(@ItemId, @AncestorId{0}, @AncestorIdText{0})",
|
||||
i.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
using (var statement = PrepareStatement(db, insertText.ToString()))
|
||||
|
|
|
@ -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" />
|
||||
|
@ -37,11 +37,11 @@
|
|||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.6" />
|
||||
<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" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.0.9" />
|
||||
<PackageReference Include="DotNet.Glob" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,11 @@ using System.Collections.Generic;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
|
@ -38,6 +37,8 @@ namespace Emby.Server.Implementations.IO
|
|||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
|
||||
/// </summary>
|
||||
|
@ -492,8 +493,6 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
}
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
|
@ -522,24 +521,4 @@ namespace Emby.Server.Implementations.IO
|
|||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public class LibraryMonitorStartup : IServerEntryPoint
|
||||
{
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
|
||||
public LibraryMonitorStartup(ILibraryMonitor monitor)
|
||||
{
|
||||
_monitor = monitor;
|
||||
}
|
||||
|
||||
public Task RunAsync()
|
||||
{
|
||||
_monitor.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
35
Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
Normal file
35
Emby.Server.Implementations/IO/LibraryMonitorStartup.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
|
||||
/// </summary>
|
||||
public sealed class LibraryMonitorStartup : IServerEntryPoint
|
||||
{
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="monitor">The library monitor.</param>
|
||||
public LibraryMonitorStartup(ILibraryMonitor monitor)
|
||||
{
|
||||
_monitor = monitor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_monitor.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user