mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-12-13 12:33:01 +03:00
Compare commits
170 Commits
v10.7.0-rc
...
v10.7.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b05f4e2b | ||
|
|
e8148ec0bd | ||
|
|
77c5c53598 | ||
|
|
d2db73b876 | ||
|
|
49873c3d7f | ||
|
|
0b577c8a44 | ||
|
|
6386606a53 | ||
|
|
cc908210d9 | ||
|
|
dec30ade8f | ||
|
|
55a8d2555e | ||
|
|
b7c3510da1 | ||
|
|
fd102abd81 | ||
|
|
f8f7767cc5 | ||
|
|
e8436814dc | ||
|
|
14f63e8f2f | ||
|
|
e764de0c80 | ||
|
|
40147c9bb7 | ||
|
|
3566d21ad1 | ||
|
|
4c8df4c5bb | ||
|
|
9798bf29f3 | ||
|
|
93ce087fc9 | ||
|
|
e39495354b | ||
|
|
ee94fad8f7 | ||
|
|
53239b0529 | ||
|
|
cf0da1de86 | ||
|
|
093510ae58 | ||
|
|
c3fafe9289 | ||
|
|
bd914acd16 | ||
|
|
81f9bec101 | ||
|
|
7db8601fbc | ||
|
|
1ec247f5d8 | ||
|
|
11e9173fbc | ||
|
|
34508286a8 | ||
|
|
a82eded845 | ||
|
|
fcb729ff6b | ||
|
|
69f30bc52c | ||
|
|
3b605b6280 | ||
|
|
e8a359f97b | ||
|
|
dbfaafc08a | ||
|
|
de6747f6c5 | ||
|
|
100fe40b0a | ||
|
|
2197d20783 | ||
|
|
f4f9ab777f | ||
|
|
9ca7d62709 | ||
|
|
7aad16b6ec | ||
|
|
f77673438e | ||
|
|
bc2eb9fa79 | ||
|
|
df69ce55f7 | ||
|
|
93cca4d50e | ||
|
|
3c64bcffe3 | ||
|
|
6ece01d425 | ||
|
|
53f333bd64 | ||
|
|
9e459090ed | ||
|
|
95a4fc0f18 | ||
|
|
62bf3db885 | ||
|
|
42d702c091 | ||
|
|
d07fe14814 | ||
|
|
970eaf8dfb | ||
|
|
bc27c2b7da | ||
|
|
37b969304a | ||
|
|
c6b5c4dda5 | ||
|
|
51f5da8015 | ||
|
|
6e89ca9a34 | ||
|
|
de1896828f | ||
|
|
7d1d159b8a | ||
|
|
c3c98331d9 | ||
|
|
e78fa8c3ef | ||
|
|
d63fb437c6 | ||
|
|
25c6388e23 | ||
|
|
8f16e10fc6 | ||
|
|
1f07586d1c | ||
|
|
5e0f480e48 | ||
|
|
210d10400a | ||
|
|
3dda25412c | ||
|
|
0183ef8e89 | ||
|
|
75f39f0f2a | ||
|
|
966217e6a9 | ||
|
|
328bcadabf | ||
|
|
0f38b2ffb2 | ||
|
|
40f4780825 | ||
|
|
546ffbe4f7 | ||
|
|
d00218c370 | ||
|
|
679d3f5873 | ||
|
|
787ad44323 | ||
|
|
2ce6b347f5 | ||
|
|
318c1f7f0c | ||
|
|
ed15cb1571 | ||
|
|
c171bac71a | ||
|
|
be5f511fc7 | ||
|
|
a65c97c8f7 | ||
|
|
3fbe10364b | ||
|
|
88ab008112 | ||
|
|
1518f6d325 | ||
|
|
53576fe1b8 | ||
|
|
da3b7bb684 | ||
|
|
4a320b26b5 | ||
|
|
63868eca40 | ||
|
|
f6e8493d69 | ||
|
|
3c3b536e81 | ||
|
|
a10eea41ac | ||
|
|
42d0c1ac5f | ||
|
|
b01290013e | ||
|
|
132335a747 | ||
|
|
75d3d120d3 | ||
|
|
e8890cc682 | ||
|
|
e4bf57c739 | ||
|
|
046dd7fa60 | ||
|
|
5e18ab3604 | ||
|
|
7545b1286b | ||
|
|
b99db64f8f | ||
|
|
20810eedbe | ||
|
|
2d88b8346d | ||
|
|
eafaccae5d | ||
|
|
d2851979d4 | ||
|
|
4b6ff7ffa5 | ||
|
|
153123278b | ||
|
|
25c19f79d4 | ||
|
|
0f139e8857 | ||
|
|
e6cc8d5015 | ||
|
|
ef864e24b9 | ||
|
|
ab054d6239 | ||
|
|
19ff447e51 | ||
|
|
4220808b96 | ||
|
|
eb0621a354 | ||
|
|
621c0b9d15 | ||
|
|
fecab1d549 | ||
|
|
bd89cdf8d2 | ||
|
|
557a091865 | ||
|
|
a1773ce97b | ||
|
|
be7411dc58 | ||
|
|
b2a8fd82d8 | ||
|
|
d53120602c | ||
|
|
da09257d58 | ||
|
|
706ac0fafd | ||
|
|
ebd4328f02 | ||
|
|
39b0d69786 | ||
|
|
9f3cebf493 | ||
|
|
20e985a0d1 | ||
|
|
5dbd6f076c | ||
|
|
19a01ccdf3 | ||
|
|
d816995d27 | ||
|
|
a934477850 | ||
|
|
a7f65bd205 | ||
|
|
8138fc3003 | ||
|
|
46a6cd8d1f | ||
|
|
524df2e45d | ||
|
|
a486cd27a9 | ||
|
|
34053b7259 | ||
|
|
d5a7478600 | ||
|
|
ed333dec43 | ||
|
|
c17c32f9dc | ||
|
|
5cc8ed6516 | ||
|
|
cdba6b3d35 | ||
|
|
fa7a8752a9 | ||
|
|
d129afa74e | ||
|
|
bc8a1d2276 | ||
|
|
147f9e1edf | ||
|
|
7796486511 | ||
|
|
ab5ae34595 | ||
|
|
81a17b803d | ||
|
|
cc6afb0971 | ||
|
|
d9a9a23a3c | ||
|
|
dd1fddf79c | ||
|
|
4df7522629 | ||
|
|
9c83a6cef9 | ||
|
|
910819c71c | ||
|
|
a0e047d560 | ||
|
|
34322ba491 | ||
|
|
801dd74ff6 | ||
|
|
129453214f |
@@ -7,7 +7,7 @@ parameters:
|
|||||||
default: "ubuntu-latest"
|
default: "ubuntu-latest"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: CompatibilityCheck
|
- job: CompatibilityCheck
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
parameters:
|
|
||||||
- name: LinuxImage
|
|
||||||
type: string
|
|
||||||
default: "ubuntu-latest"
|
|
||||||
- name: GeneratorVersion
|
|
||||||
type: string
|
|
||||||
default: "5.0.0-beta2"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
- job: GenerateApiClients
|
|
||||||
displayName: 'Generate Api Clients'
|
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
|
||||||
dependsOn: Test
|
|
||||||
|
|
||||||
pool:
|
|
||||||
vmImage: "${{ parameters.LinuxImage }}"
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- task: DownloadPipelineArtifact@2
|
|
||||||
displayName: 'Download OpenAPI Spec Artifact'
|
|
||||||
inputs:
|
|
||||||
source: 'current'
|
|
||||||
artifact: "OpenAPI Spec"
|
|
||||||
path: "$(System.ArtifactsDirectory)/openapispec"
|
|
||||||
runVersion: "latest"
|
|
||||||
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Download OpenApi Generator'
|
|
||||||
inputs:
|
|
||||||
script: "wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${{ parameters.GeneratorVersion }}/openapi-generator-cli-${{ parameters.GeneratorVersion }}.jar -O openapi-generator-cli.jar"
|
|
||||||
|
|
||||||
## Authenticate with npm registry
|
|
||||||
- task: npmAuthenticate@0
|
|
||||||
inputs:
|
|
||||||
workingFile: ./.npmrc
|
|
||||||
customEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
|
|
||||||
## Generate npm api client
|
|
||||||
- task: CmdLine@2
|
|
||||||
displayName: 'Build stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
script: "bash ./apiclient/templates/typescript/axios/generate.sh $(System.ArtifactsDirectory)"
|
|
||||||
|
|
||||||
## Run npm install
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Install npm dependencies'
|
|
||||||
inputs:
|
|
||||||
command: install
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
|
|
||||||
## Publish npm packages
|
|
||||||
- task: Npm@1
|
|
||||||
displayName: 'Publish stable typescript axios client'
|
|
||||||
inputs:
|
|
||||||
command: custom
|
|
||||||
customCommand: publish --access public
|
|
||||||
publishRegistry: useExternalRegistry
|
|
||||||
publishEndpoint: 'jellyfin-bot for NPM'
|
|
||||||
workingDir: ./apiclient/generated/typescript/axios
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
parameters:
|
parameters:
|
||||||
LinuxImage: 'ubuntu-latest'
|
LinuxImage: 'ubuntu-latest'
|
||||||
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
RestoreBuildProjects: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
DotNetSdkVersion: 5.0.100
|
DotNetSdkVersion: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Build
|
- job: Build
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ jobs:
|
|||||||
dependsOn:
|
dependsOn:
|
||||||
- BuildPackage
|
- BuildPackage
|
||||||
- BuildDocker
|
- BuildDocker
|
||||||
condition: and(succeeded('BuildPackage'), succeeded('BuildDocker'))
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@@ -186,13 +185,14 @@ jobs:
|
|||||||
|
|
||||||
- job: PublishNuget
|
- job: PublishNuget
|
||||||
displayName: 'Publish NuGet packages'
|
displayName: 'Publish NuGet packages'
|
||||||
dependsOn:
|
|
||||||
- BuildPackage
|
|
||||||
condition: succeeded('BuildPackage')
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
|
|
||||||
|
variables:
|
||||||
|
- name: JellyfinVersion
|
||||||
|
value: $[replace(variables['Build.SourceBranch'],'refs/tags/v','')]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Use .NET 5.0 sdk'
|
displayName: 'Use .NET 5.0 sdk'
|
||||||
@@ -204,12 +204,19 @@ jobs:
|
|||||||
displayName: 'Build Stable Nuget packages'
|
displayName: 'Build Stable Nuget packages'
|
||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'pack'
|
command: 'custom'
|
||||||
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'
|
projects: |
|
||||||
versioningScheme: 'off'
|
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: -o $(Build.ArtifactStagingDirectory) -p:Version=$(JellyfinVersion)
|
||||||
|
|
||||||
- task: DotNetCoreCLI@2
|
- task: DotNetCoreCLI@2
|
||||||
displayName: 'Build Unstable Nuget packages'
|
displayName: 'Build Unstable Nuget packages'
|
||||||
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'custom'
|
command: 'custom'
|
||||||
projects: |
|
projects: |
|
||||||
@@ -232,7 +239,7 @@ jobs:
|
|||||||
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
|
||||||
inputs:
|
inputs:
|
||||||
command: 'push'
|
command: 'push'
|
||||||
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;$(Build.ArtifactStagingDirectory)/**/*.snupkg'
|
packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg'
|
||||||
nuGetFeedType: 'external'
|
nuGetFeedType: 'external'
|
||||||
publishFeedCredentials: 'NugetOrg'
|
publishFeedCredentials: 'NugetOrg'
|
||||||
allowPackageConflicts: true # This ignores an error if the version already exists
|
allowPackageConflicts: true # This ignores an error if the version already exists
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ parameters:
|
|||||||
default: "tests/**/*Tests.csproj"
|
default: "tests/**/*Tests.csproj"
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
type: string
|
type: string
|
||||||
default: 5.0.100
|
default: 5.0.103
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Test
|
- job: Test
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ variables:
|
|||||||
- name: RestoreBuildProjects
|
- name: RestoreBuildProjects
|
||||||
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
value: 'Jellyfin.Server/Jellyfin.Server.csproj'
|
||||||
- name: DotNetSdkVersion
|
- name: DotNetSdkVersion
|
||||||
value: 5.0.100
|
value: 5.0.103
|
||||||
|
|
||||||
pr:
|
pr:
|
||||||
autoCancel: true
|
autoCancel: true
|
||||||
@@ -61,6 +61,3 @@ jobs:
|
|||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), 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
|
- template: azure-pipelines-package.yml
|
||||||
|
|
||||||
- ${{ if or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), startsWith(variables['Build.SourceBranch'], 'refs/heads/master')) }}:
|
|
||||||
- template: azure-pipelines-api-client.yml
|
|
||||||
|
|||||||
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: '5.0.100'
|
dotnet-version: '5.0.x'
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
- [h1nk](https://github.com/h1nk)
|
- [h1nk](https://github.com/h1nk)
|
||||||
- [hawken93](https://github.com/hawken93)
|
- [hawken93](https://github.com/hawken93)
|
||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
@@ -103,6 +104,7 @@
|
|||||||
- [shemanaev](https://github.com/shemanaev)
|
- [shemanaev](https://github.com/shemanaev)
|
||||||
- [skaro13](https://github.com/skaro13)
|
- [skaro13](https://github.com/skaro13)
|
||||||
- [sl1288](https://github.com/sl1288)
|
- [sl1288](https://github.com/sl1288)
|
||||||
|
- [Smith00101010](https://github.com/Smith00101010)
|
||||||
- [sorinyo2004](https://github.com/sorinyo2004)
|
- [sorinyo2004](https://github.com/sorinyo2004)
|
||||||
- [sparky8251](https://github.com/sparky8251)
|
- [sparky8251](https://github.com/sparky8251)
|
||||||
- [spookbits](https://github.com/spookbits)
|
- [spookbits](https://github.com/spookbits)
|
||||||
@@ -141,6 +143,8 @@
|
|||||||
- [Pusta](https://github.com/pusta)
|
- [Pusta](https://github.com/pusta)
|
||||||
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
- [nielsvanvelzen](https://github.com/nielsvanvelzen)
|
||||||
- [skyfrk](https://github.com/skyfrk)
|
- [skyfrk](https://github.com/skyfrk)
|
||||||
|
- [ianjazz246](https://github.com/ianjazz246)
|
||||||
|
- [peterspenler](https://github.com/peterspenler)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
using (StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8))
|
||||||
{
|
{
|
||||||
|
// If this using are changed to single lines, then write.Flush needs to be appended before the return.
|
||||||
using (var writer = XmlWriter.Create(builder, settings))
|
using (var writer = XmlWriter.Create(builder, settings))
|
||||||
{
|
{
|
||||||
// writer.WriteStartDocument();
|
// writer.WriteStartDocument();
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ namespace Emby.Dlna
|
|||||||
|
|
||||||
if (profile != null)
|
if (profile != null)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found matching device profile: {0}", profile.Name);
|
_logger.LogDebug("Found matching device profile: {ProfileName}", profile.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -138,80 +138,45 @@ namespace Emby.Dlna
|
|||||||
_logger.LogInformation(builder.ToString());
|
_logger.LogInformation(builder.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
/// <summary>
|
||||||
|
/// Attempts to match a device with a profile.
|
||||||
|
/// Rules:
|
||||||
|
/// - If the profile field has no value, the field matches irregardless of its contents.
|
||||||
|
/// - the profile field can be an exact match, or a reg exp.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceInfo">The <see cref="DeviceIdentification"/> of the device.</param>
|
||||||
|
/// <param name="profileInfo">The <see cref="DeviceIdentification"/> of the profile.</param>
|
||||||
|
/// <returns><b>True</b> if they match.</returns>
|
||||||
|
public bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(profileInfo.FriendlyName))
|
return IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer)
|
||||||
if (deviceInfo.FriendlyName == null || !IsRegexOrSubstringMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
&& IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl)
|
||||||
{
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription)
|
||||||
return false;
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber)
|
||||||
}
|
&& IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl)
|
||||||
|
&& IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber);
|
||||||
if (!string.IsNullOrEmpty(profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
if (deviceInfo.Manufacturer == null || !IsRegexOrSubstringMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ManufacturerUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelDescription == null || !IsRegexOrSubstringMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelName == null || !IsRegexOrSubstringMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelNumber == null || !IsRegexOrSubstringMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
if (deviceInfo.ModelUrl == null || !IsRegexOrSubstringMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
if (deviceInfo.SerialNumber == null || !IsRegexOrSubstringMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
private bool IsRegexOrSubstringMatch(string input, string pattern)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(pattern))
|
||||||
|
{
|
||||||
|
// In profile identification: An empty pattern matches anything.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
{
|
||||||
|
// The profile contains a value, and the device doesn't.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return input.Contains(pattern, StringComparison.OrdinalIgnoreCase) || Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
return input.Equals(pattern, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
}
|
}
|
||||||
catch (ArgumentException ex)
|
catch (ArgumentException ex)
|
||||||
{
|
{
|
||||||
@@ -333,7 +298,12 @@ namespace Emby.Dlna
|
|||||||
throw new ArgumentNullException(nameof(id));
|
throw new ArgumentNullException(nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
var info = GetProfileInfosInternal().FirstOrDefault(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (info == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return ParseProfileFile(info.Path, info.Info.Type);
|
return ParseProfileFile(info.Path, info.Info.Type);
|
||||||
}
|
}
|
||||||
@@ -395,7 +365,8 @@ namespace Emby.Dlna
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(systemProfilesPath);
|
Directory.CreateDirectory(systemProfilesPath);
|
||||||
|
|
||||||
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ namespace Emby.Dlna.Main
|
|||||||
|
|
||||||
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
_netConfig = config.GetConfiguration<NetworkConfiguration>("network");
|
||||||
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
_disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
|
||||||
if (_disabled)
|
|
||||||
|
if (_disabled && _config.GetDlnaConfiguration().EnableServer)
|
||||||
{
|
{
|
||||||
_logger.LogError("The DLNA specification does not support HTTPS.");
|
_logger.LogError("The DLNA specification does not support HTTPS.");
|
||||||
}
|
}
|
||||||
@@ -228,7 +229,10 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
if (communicationsServer != null)
|
||||||
|
{
|
||||||
|
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -313,9 +317,12 @@ namespace Emby.Dlna.Main
|
|||||||
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
_logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
|
||||||
|
|
||||||
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
|
||||||
// DLNA will only work over http, so we must reset to http:// : {port}
|
if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
|
||||||
uri.Scheme = "http://";
|
{
|
||||||
uri.Port = _netConfig.PublicPort;
|
// DLNA will only work over http, so we must reset to http:// : {port}.
|
||||||
|
uri.Scheme = "http";
|
||||||
|
uri.Port = _netConfig.HttpServerPortNumber;
|
||||||
|
}
|
||||||
|
|
||||||
var device = new SsdpRootDevice
|
var device = new SsdpRootDevice
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -253,7 +253,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -278,7 +278,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Seek");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -305,7 +305,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
_logger.LogDebug("{0} - SetAvTransport Uri: {1} DlnaHeaders: {2}", Properties.Name, url, header);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "SetAVTransportURI");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -378,6 +378,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
public async Task SetPlay(CancellationToken cancellationToken)
|
public async Task SetPlay(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (avCommands == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
await SetPlay(avCommands, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -388,7 +392,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Stop");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -406,7 +410,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => c.Name == "Pause");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -528,7 +532,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetVolume");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -578,7 +582,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var command = rendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
var command = rendererCommands?.ServiceActions.FirstOrDefault(c => c.Name == "GetMute");
|
||||||
if (command == null)
|
if (command == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -665,6 +669,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
@@ -733,6 +741,11 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var rendererCommands = await GetRenderingProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (rendererCommands == null)
|
||||||
|
{
|
||||||
|
return (false, null);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
var result = await new SsdpHttpClient(_httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
@@ -914,6 +927,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
|
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
AvCommands = TransportCommands.Create(document);
|
AvCommands = TransportCommands.Create(document);
|
||||||
return AvCommands;
|
return AvCommands;
|
||||||
@@ -942,6 +959,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
var httpClient = new SsdpHttpClient(_httpClientFactory);
|
||||||
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
_logger.LogDebug("Dlna Device.GetRenderingProtocolAsync");
|
||||||
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
var document = await httpClient.GetDataAsync(url, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
RendererCommands = TransportCommands.Create(document);
|
RendererCommands = TransportCommands.Create(document);
|
||||||
return RendererCommands;
|
return RendererCommands;
|
||||||
@@ -973,6 +994,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
var ssdpHttpClient = new SsdpHttpClient(httpClientFactory);
|
||||||
|
|
||||||
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
var document = await ssdpHttpClient.GetDataAsync(url.ToString(), cancellationToken).ConfigureAwait(false);
|
||||||
|
if (document == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var friendlyNames = new List<string>();
|
var friendlyNames = new List<string>();
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -826,7 +826,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
return SendPlayCommand(data as PlayRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name == SessionMessageType.PlayState)
|
if (name == SessionMessageType.Playstate)
|
||||||
{
|
{
|
||||||
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
|
||||||
}
|
}
|
||||||
@@ -896,16 +896,16 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var parts = url.Split('/');
|
var parts = url.Split('/');
|
||||||
|
|
||||||
for (var i = 0; i < parts.Length; i++)
|
for (var i = 0; i < parts.Length - 1; i++)
|
||||||
{
|
{
|
||||||
var part = parts[i];
|
var part = parts[i];
|
||||||
|
|
||||||
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (parts.Length > i + 1)
|
if (Guid.TryParse(parts[i + 1], out var result))
|
||||||
{
|
{
|
||||||
return Guid.Parse(parts[i + 1]);
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -943,11 +943,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
request.DeviceId = values.GetValueOrDefault("DeviceId");
|
||||||
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
request.MediaSourceId = values.GetValueOrDefault("MediaSourceId");
|
||||||
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
request.LiveStreamId = values.GetValueOrDefault("LiveStreamId");
|
||||||
|
request.IsDirectStream = string.Equals("true", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
||||||
// Be careful, IsDirectStream==true by default (Static != false or not in query).
|
|
||||||
// See initialization of StreamingRequestDto in AudioController.GetAudioStream() method : Static = @static ?? true.
|
|
||||||
request.IsDirectStream = !string.Equals("false", values.GetValueOrDefault("Static"), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
request.AudioStreamIndex = GetIntValue(values, "AudioStreamIndex");
|
||||||
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
request.SubtitleStreamIndex = GetIntValue(values, "SubtitleStreamIndex");
|
||||||
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
request.StartPositionTicks = GetLongValue(values, "StartPositionTicks");
|
||||||
|
|||||||
@@ -178,12 +178,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
if (controller == null)
|
if (controller == null)
|
||||||
{
|
{
|
||||||
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
var device = await Device.CreateuPnpDeviceAsync(uri, _httpClientFactory, _logger, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (device == null)
|
||||||
|
{
|
||||||
|
_logger.LogError("Ignoring device as xml response is invalid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string deviceName = device.Properties.Name;
|
string deviceName = device.Properties.Name;
|
||||||
|
|
||||||
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
_sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
|
||||||
|
|
||||||
string serverAddress = _appHost.GetSmartApiUrl(info.LocalIpAddress);
|
string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
|
||||||
|
|
||||||
controller = new PlayToController(
|
controller = new PlayToController(
|
||||||
sessionInfo,
|
sessionInfo,
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
cancellationToken)
|
cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
return await XDocument.LoadAsync(
|
||||||
return XDocument.Parse(
|
stream,
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
LoadOptions.PreserveWhitespace,
|
||||||
LoadOptions.PreserveWhitespace);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
|
||||||
@@ -94,10 +94,17 @@ namespace Emby.Dlna.PlayTo
|
|||||||
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
options.Headers.TryAddWithoutValidation("FriendlyName.DLNA.ORG", FriendlyName);
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).SendAsync(options, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
try
|
||||||
return XDocument.Parse(
|
{
|
||||||
await reader.ReadToEndAsync().ConfigureAwait(false),
|
return await XDocument.LoadAsync(
|
||||||
LoadOptions.PreserveWhitespace);
|
stream,
|
||||||
|
LoadOptions.PreserveWhitespace,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
private async Task<HttpResponseMessage> PostSoapDataAsync(
|
||||||
|
|||||||
@@ -13,12 +13,10 @@ namespace Emby.Dlna.PlayTo
|
|||||||
public class TransportCommands
|
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 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>();
|
|
||||||
private List<ServiceAction> _serviceActions = new List<ServiceAction>();
|
|
||||||
|
|
||||||
public List<StateVariable> StateVariables => _stateVariables;
|
public List<StateVariable> StateVariables { get; } = new List<StateVariable>();
|
||||||
|
|
||||||
public List<ServiceAction> ServiceActions => _serviceActions;
|
public List<ServiceAction> ServiceActions { get; } = new List<ServiceAction>();
|
||||||
|
|
||||||
public static TransportCommands Create(XDocument document)
|
public static TransportCommands Create(XDocument document)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ namespace Emby.Dlna.Profiles
|
|||||||
{
|
{
|
||||||
public DefaultProfile()
|
public DefaultProfile()
|
||||||
{
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
|
||||||
Name = "Generic Device";
|
Name = "Generic Device";
|
||||||
|
|
||||||
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
ProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma:*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*:image/jpeg:*,http-get:*:image/png:*,http-get:*:image/gif:*,http-get:*:image/tiff:*";
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace Emby.Dlna.Ssdp
|
|||||||
{
|
{
|
||||||
lock (_syncLock)
|
lock (_syncLock)
|
||||||
{
|
{
|
||||||
if (_listenerCount > 0 && _deviceLocator == null)
|
if (_listenerCount > 0 && _deviceLocator == null && _commsServer != null)
|
||||||
{
|
{
|
||||||
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
_deviceLocator = new SsdpDeviceLocator(_commsServer);
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ namespace Emby.Dlna.Ssdp
|
|||||||
{
|
{
|
||||||
Location = e.DiscoveredDevice.DescriptionLocation,
|
Location = e.DiscoveredDevice.DescriptionLocation,
|
||||||
Headers = headers,
|
Headers = headers,
|
||||||
LocalIpAddress = e.LocalIpAddress
|
RemoteIpAddress = e.RemoteIpAddress
|
||||||
});
|
});
|
||||||
|
|
||||||
DeviceDiscoveredInternal?.Invoke(this, args);
|
DeviceDiscoveredInternal?.Invoke(this, args);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
@@ -171,11 +172,26 @@ namespace Emby.Drawing
|
|||||||
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
return (originalImagePath, MimeTypes.GetMimeType(originalImagePath), dateModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageDimensions newSize = ImageHelper.GetNewImageSize(options, null);
|
|
||||||
int quality = options.Quality;
|
int quality = options.Quality;
|
||||||
|
|
||||||
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
ImageFormat outputFormat = GetOutputFormat(options.SupportedOutputFormats, requiresTransparency);
|
||||||
string cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer);
|
string cacheFilePath = GetCacheFilePath(
|
||||||
|
originalImagePath,
|
||||||
|
options.Width,
|
||||||
|
options.Height,
|
||||||
|
options.MaxWidth,
|
||||||
|
options.MaxHeight,
|
||||||
|
options.FillWidth,
|
||||||
|
options.FillHeight,
|
||||||
|
quality,
|
||||||
|
dateModified,
|
||||||
|
outputFormat,
|
||||||
|
options.AddPlayedIndicator,
|
||||||
|
options.PercentPlayed,
|
||||||
|
options.UnplayedCount,
|
||||||
|
options.Blur,
|
||||||
|
options.BackgroundColor,
|
||||||
|
options.ForegroundLayer);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -246,48 +262,111 @@ namespace Emby.Drawing
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache file path based on a set of parameters.
|
/// Gets the cache file path based on a set of parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer)
|
private string GetCacheFilePath(
|
||||||
|
string originalPath,
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
int? maxWidth,
|
||||||
|
int? maxHeight,
|
||||||
|
int? fillWidth,
|
||||||
|
int? fillHeight,
|
||||||
|
int quality,
|
||||||
|
DateTime dateModified,
|
||||||
|
ImageFormat format,
|
||||||
|
bool addPlayedIndicator,
|
||||||
|
double percentPlayed,
|
||||||
|
int? unwatchedCount,
|
||||||
|
int? blur,
|
||||||
|
string backgroundColor,
|
||||||
|
string foregroundLayer)
|
||||||
{
|
{
|
||||||
var filename = originalPath
|
var filename = new StringBuilder(256);
|
||||||
+ "width=" + outputSize.Width
|
filename.Append(originalPath);
|
||||||
+ "height=" + outputSize.Height
|
|
||||||
+ "quality=" + quality
|
filename.Append(",quality=");
|
||||||
+ "datemodified=" + dateModified.Ticks
|
filename.Append(quality);
|
||||||
+ "f=" + format;
|
|
||||||
|
filename.Append(",datemodified=");
|
||||||
|
filename.Append(dateModified.Ticks);
|
||||||
|
|
||||||
|
filename.Append(",f=");
|
||||||
|
filename.Append(format);
|
||||||
|
|
||||||
|
if (width.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",width=");
|
||||||
|
filename.Append(width.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",height=");
|
||||||
|
filename.Append(height.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxwidth=");
|
||||||
|
filename.Append(maxWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",maxheight=");
|
||||||
|
filename.Append(maxHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillWidth.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillwidth=");
|
||||||
|
filename.Append(fillWidth.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fillHeight.HasValue)
|
||||||
|
{
|
||||||
|
filename.Append(",fillheight=");
|
||||||
|
filename.Append(fillHeight.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (addPlayedIndicator)
|
if (addPlayedIndicator)
|
||||||
{
|
{
|
||||||
filename += "pl=true";
|
filename.Append(",pl=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (percentPlayed > 0)
|
if (percentPlayed > 0)
|
||||||
{
|
{
|
||||||
filename += "p=" + percentPlayed;
|
filename.Append(",p=");
|
||||||
|
filename.Append(percentPlayed);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (unwatchedCount.HasValue)
|
if (unwatchedCount.HasValue)
|
||||||
{
|
{
|
||||||
filename += "p=" + unwatchedCount.Value;
|
filename.Append(",p=");
|
||||||
|
filename.Append(unwatchedCount.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blur.HasValue)
|
if (blur.HasValue)
|
||||||
{
|
{
|
||||||
filename += "blur=" + blur.Value;
|
filename.Append(",blur=");
|
||||||
|
filename.Append(blur.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(backgroundColor))
|
if (!string.IsNullOrEmpty(backgroundColor))
|
||||||
{
|
{
|
||||||
filename += "b=" + backgroundColor;
|
filename.Append(",b=");
|
||||||
|
filename.Append(backgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(foregroundLayer))
|
if (!string.IsNullOrEmpty(foregroundLayer))
|
||||||
{
|
{
|
||||||
filename += "fl=" + foregroundLayer;
|
filename.Append(",fl=");
|
||||||
|
filename.Append(foregroundLayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
filename += "v=" + Version;
|
filename.Append(",v=");
|
||||||
|
filename.Append(Version);
|
||||||
|
|
||||||
return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant());
|
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Jellyfin Contributors</Authors>
|
<Authors>Jellyfin Contributors</Authors>
|
||||||
<PackageId>Jellyfin.Naming</PackageId>
|
<PackageId>Jellyfin.Naming</PackageId>
|
||||||
<VersionPrefix>10.7.0</VersionPrefix>
|
<VersionPrefix>10.7.6</VersionPrefix>
|
||||||
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
private static bool TryClean(string name, Regex expression, out ReadOnlySpan<char> newName)
|
||||||
{
|
{
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var match = expression.Match(name);
|
var match = expression.Match(name);
|
||||||
int index = match.Index;
|
int index = match.Index;
|
||||||
if (match.Success && index != 0)
|
if (match.Success && index != 0)
|
||||||
@@ -41,7 +47,7 @@ namespace Emby.Naming.Video
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
newName = string.Empty;
|
newName = ReadOnlySpan<char>.Empty;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,20 +222,21 @@ namespace Emby.Naming.Video
|
|||||||
|
|
||||||
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
if (testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
// Remove the folder name before cleaning as we don't care about cleaning that part
|
||||||
{
|
|
||||||
testFilename = cleanName.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (folderName.Length <= testFilename.Length)
|
if (folderName.Length <= testFilename.Length)
|
||||||
{
|
{
|
||||||
testFilename = testFilename.Substring(folderName.Length).Trim();
|
testFilename = testFilename.Substring(folderName.Length).Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CleanStringParser.TryClean(testFilename, _options.CleanStringRegexes, out var cleanName))
|
||||||
|
{
|
||||||
|
testFilename = cleanName.Trim().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CleanStringParser should have removed common keywords etc.
|
||||||
return string.IsNullOrEmpty(testFilename)
|
return string.IsNullOrEmpty(testFilename)
|
||||||
|| testFilename[0].Equals('-')
|
|| testFilename[0] == '-'
|
||||||
|| testFilename[0].Equals('_')
|
|| Regex.IsMatch(testFilename, @"^\[([^]]*)\]");
|
||||||
|| string.IsNullOrWhiteSpace(Regex.Replace(testFilename, @"\[([^]]*)\]", string.Empty));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -75,10 +75,6 @@ namespace Emby.Notifications
|
|||||||
Type = NotificationType.VideoPlaybackStopped.ToString()
|
Type = NotificationType.VideoPlaybackStopped.ToString()
|
||||||
},
|
},
|
||||||
new NotificationTypeInfo
|
new NotificationTypeInfo
|
||||||
{
|
|
||||||
Type = NotificationType.CameraImageUploaded.ToString()
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
{
|
||||||
Type = NotificationType.UserLockedOut.ToString()
|
Type = NotificationType.UserLockedOut.ToString()
|
||||||
},
|
},
|
||||||
@@ -114,10 +110,6 @@ namespace Emby.Notifications
|
|||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("Plugin");
|
note.Category = _localization.GetLocalizedString("Plugin");
|
||||||
}
|
}
|
||||||
else if (note.Type.IndexOf("CameraImageUploaded", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("Sync");
|
|
||||||
}
|
|
||||||
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
note.Category = _localization.GetLocalizedString("User");
|
note.Category = _localization.GetLocalizedString("User");
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
// Save it after load in case we got new items
|
// Save it after load in case we got new items
|
||||||
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
fs.Write(newBytes, 0, newBytesLen);
|
fs.Write(newBytes, 0, newBytesLen);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ using Emby.Server.Implementations.Serialization;
|
|||||||
using Emby.Server.Implementations.Session;
|
using Emby.Server.Implementations.Session;
|
||||||
using Emby.Server.Implementations.SyncPlay;
|
using Emby.Server.Implementations.SyncPlay;
|
||||||
using Emby.Server.Implementations.TV;
|
using Emby.Server.Implementations.TV;
|
||||||
|
using Emby.Server.Implementations.Udp;
|
||||||
using Emby.Server.Implementations.Updates;
|
using Emby.Server.Implementations.Updates;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
@@ -98,6 +99,7 @@ using MediaBrowser.Providers.Subtitles;
|
|||||||
using MediaBrowser.XbmcMetadata.Providers;
|
using MediaBrowser.XbmcMetadata.Providers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Prometheus.DotNetRuntime;
|
using Prometheus.DotNetRuntime;
|
||||||
@@ -117,6 +119,7 @@ namespace Emby.Server.Implementations
|
|||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
|
private readonly IConfiguration _startupConfig;
|
||||||
private readonly IXmlSerializer _xmlSerializer;
|
private readonly IXmlSerializer _xmlSerializer;
|
||||||
private readonly IJsonSerializer _jsonSerializer;
|
private readonly IJsonSerializer _jsonSerializer;
|
||||||
private readonly IStartupOptions _startupOptions;
|
private readonly IStartupOptions _startupOptions;
|
||||||
@@ -227,6 +230,11 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int HttpsPort { get; private set; }
|
public int HttpsPort { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the PublishedServerUrl setting.
|
||||||
|
/// </summary>
|
||||||
|
public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the server configuration manager.
|
/// Gets the server configuration manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -239,12 +247,14 @@ namespace Emby.Server.Implementations
|
|||||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||||
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
/// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
|
||||||
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
/// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
|
||||||
|
/// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
|
||||||
public ApplicationHost(
|
public ApplicationHost(
|
||||||
IServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
IStartupOptions options,
|
IStartupOptions options,
|
||||||
|
IConfiguration startupConfig,
|
||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IServiceCollection serviceCollection)
|
IServiceCollection serviceCollection)
|
||||||
{
|
{
|
||||||
@@ -268,6 +278,7 @@ namespace Emby.Server.Implementations
|
|||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
|
|
||||||
_startupOptions = options;
|
_startupOptions = options;
|
||||||
|
_startupConfig = startupConfig;
|
||||||
|
|
||||||
// Initialize runtime stat collection
|
// Initialize runtime stat collection
|
||||||
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
if (ServerConfigurationManager.Configuration.EnableMetrics)
|
||||||
@@ -1148,10 +1159,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
public string GetSmartApiUrl(IPAddress ipAddress, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
string smart = NetManager.GetBindInterface(ipAddress, out port);
|
||||||
@@ -1168,10 +1179,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
public string GetSmartApiUrl(HttpRequest request, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(request, out port);
|
string smart = NetManager.GetBindInterface(request, out port);
|
||||||
@@ -1188,10 +1199,10 @@ namespace Emby.Server.Implementations
|
|||||||
public string GetSmartApiUrl(string hostname, int? port = null)
|
public string GetSmartApiUrl(string hostname, int? port = null)
|
||||||
{
|
{
|
||||||
// Published server ends with a /
|
// Published server ends with a /
|
||||||
if (_startupOptions.PublishedServerUrl != null)
|
if (!string.IsNullOrEmpty(PublishedServerUrl))
|
||||||
{
|
{
|
||||||
// Published server ends with a '/', so we need to remove it.
|
// Published server ends with a '/', so we need to remove it.
|
||||||
return _startupOptions.PublishedServerUrl.ToString().Trim('/');
|
return PublishedServerUrl.Trim('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
string smart = NetManager.GetBindInterface(hostname, out port);
|
string smart = NetManager.GetBindInterface(hostname, out port);
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
var name = _localizationManager.GetLocalizedString("Collections");
|
var name = _localizationManager.GetLocalizedString("Collections");
|
||||||
|
|
||||||
await _libraryManager.AddVirtualFolder(name, CollectionType.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
await _libraryManager.AddVirtualFolder(name, CollectionTypeOptions.BoxSets, libraryOptions, true).ConfigureAwait(false);
|
||||||
|
|
||||||
return FindFolders(path).First();
|
return FindFolders(path).First();
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
|
|
||||||
private IEnumerable<BoxSet> GetCollections(User user)
|
private IEnumerable<BoxSet> GetCollections(User user)
|
||||||
{
|
{
|
||||||
var folder = GetCollectionsFolder(false).Result;
|
var folder = GetCollectionsFolder(false).GetAwaiter().GetResult();
|
||||||
|
|
||||||
return folder == null
|
return folder == null
|
||||||
? Enumerable.Empty<BoxSet>()
|
? Enumerable.Empty<BoxSet>()
|
||||||
@@ -319,11 +319,11 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
{
|
{
|
||||||
var results = new Dictionary<Guid, BaseItem>();
|
var results = new Dictionary<Guid, BaseItem>();
|
||||||
|
|
||||||
var allBoxsets = GetCollections(user).ToList();
|
var allBoxSets = GetCollections(user).ToList();
|
||||||
|
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
if (!(item is ISupportsBoxSetGrouping))
|
if (item is not ISupportsBoxSetGrouping)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[item.Id] = item;
|
||||||
}
|
}
|
||||||
@@ -331,20 +331,44 @@ namespace Emby.Server.Implementations.Collections
|
|||||||
{
|
{
|
||||||
var itemId = item.Id;
|
var itemId = item.Id;
|
||||||
|
|
||||||
var currentBoxSets = allBoxsets
|
var itemIsInBoxSet = false;
|
||||||
.Where(i => i.ContainsLinkedChildByItemId(itemId))
|
foreach (var boxSet in allBoxSets)
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (currentBoxSets.Count > 0)
|
|
||||||
{
|
{
|
||||||
foreach (var boxset in currentBoxSets)
|
if (!boxSet.ContainsLinkedChildByItemId(itemId))
|
||||||
{
|
{
|
||||||
results[boxset.Id] = boxset;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemIsInBoxSet = true;
|
||||||
|
|
||||||
|
results.TryAdd(boxSet.Id, boxSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip any item that is in a box set
|
||||||
|
if (itemIsInBoxSet)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alreadyInResults = false;
|
||||||
|
// this is kind of a performance hack because only Video has alternate versions that should be in a box set?
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
foreach (var childId in video.GetLocalAlternateVersionIds())
|
||||||
|
{
|
||||||
|
if (!results.ContainsKey(childId))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
alreadyInResults = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
if (!alreadyInResults)
|
||||||
{
|
{
|
||||||
results[item.Id] = item;
|
results[itemId] = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5415,7 +5415,6 @@ AND Type = @InternalPersonType)");
|
|||||||
ItemIds = query.ItemIds,
|
ItemIds = query.ItemIds,
|
||||||
TopParentIds = query.TopParentIds,
|
TopParentIds = query.TopParentIds,
|
||||||
ParentId = query.ParentId,
|
ParentId = query.ParentId,
|
||||||
IsPlayed = query.IsPlayed,
|
|
||||||
IsAiring = query.IsAiring,
|
IsAiring = query.IsAiring,
|
||||||
IsMovie = query.IsMovie,
|
IsMovie = query.IsMovie,
|
||||||
IsSports = query.IsSports,
|
IsSports = query.IsSports,
|
||||||
@@ -5441,6 +5440,7 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
var outerQuery = new InternalItemsQuery(query.User)
|
var outerQuery = new InternalItemsQuery(query.User)
|
||||||
{
|
{
|
||||||
|
IsPlayed = query.IsPlayed,
|
||||||
IsFavorite = query.IsFavorite,
|
IsFavorite = query.IsFavorite,
|
||||||
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
|
IsFavoriteOrLiked = query.IsFavoriteOrLiked,
|
||||||
IsLiked = query.IsLiked,
|
IsLiked = query.IsLiked,
|
||||||
|
|||||||
@@ -582,7 +582,26 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
{
|
{
|
||||||
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
|
||||||
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
|
baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||||
baseItemPerson.ImageBlurHashes = dto.ImageBlurHashes;
|
if (dto.ImageBlurHashes != null)
|
||||||
|
{
|
||||||
|
// Only add BlurHash for the person's image.
|
||||||
|
baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
|
||||||
|
foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
|
||||||
|
{
|
||||||
|
if (blurHash != null)
|
||||||
|
{
|
||||||
|
baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
|
||||||
|
foreach (var (imageId, blurHashValue) in blurHash)
|
||||||
|
{
|
||||||
|
if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
baseItemPerson.ImageBlurHashes[imageType][imageId] = blurHashValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list.Add(baseItemPerson);
|
list.Add(baseItemPerson);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1138,7 +1157,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (episodeSeries != null)
|
if (episodeSeries != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, episodeSeries, ImageType.Primary);
|
||||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||||
{
|
{
|
||||||
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
AttachPrimaryImageAspectRatio(dto, episodeSeries);
|
||||||
}
|
}
|
||||||
@@ -1188,7 +1207,7 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
if (series != null)
|
if (series != null)
|
||||||
{
|
{
|
||||||
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
dto.SeriesPrimaryImageTag = GetTagAndFillBlurhash(dto, series, ImageType.Primary);
|
||||||
if (!dto.ImageTags.ContainsKey(ImageType.Primary))
|
if (dto.ImageTags == null || !dto.ImageTags.ContainsKey(ImageType.Primary))
|
||||||
{
|
{
|
||||||
AttachPrimaryImageAspectRatio(dto, series);
|
AttachPrimaryImageAspectRatio(dto, series);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,8 +106,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
NatUtility.StartDiscovery();
|
NatUtility.StartDiscovery();
|
||||||
|
|
||||||
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
_timer = new Timer((_) => _createdRules.Clear(), null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Stop()
|
private void Stop()
|
||||||
@@ -118,13 +116,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
NatUtility.DeviceFound -= OnNatUtilityDeviceFound;
|
||||||
|
|
||||||
_timer?.Dispose();
|
_timer?.Dispose();
|
||||||
|
|
||||||
_deviceDiscovery.DeviceDiscovered -= OnDeviceDiscoveryDeviceDiscovered;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
|
||||||
{
|
|
||||||
NatUtility.Search(e.Argument.LocalIpAddress, NatProtocol.Upnp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
private async void OnNatUtilityDeviceFound(object sender, DeviceEventArgs e)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -29,7 +31,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The UDP server.
|
/// The UDP server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private UdpServer _udpServer;
|
private UdpServer? _udpServer;
|
||||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
@@ -71,9 +73,8 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||||||
}
|
}
|
||||||
|
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
_udpServer.Dispose();
|
|
||||||
_cancellationTokenSource.Dispose();
|
_cancellationTokenSource.Dispose();
|
||||||
_cancellationTokenSource = null;
|
_udpServer?.Dispose();
|
||||||
_udpServer = null;
|
_udpServer = null;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|||||||
@@ -14,15 +14,18 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
public class WebSocketManager : IWebSocketManager
|
public class WebSocketManager : IWebSocketManager
|
||||||
{
|
{
|
||||||
private readonly IWebSocketListener[] _webSocketListeners;
|
private readonly IWebSocketListener[] _webSocketListeners;
|
||||||
|
private readonly IAuthService _authService;
|
||||||
private readonly ILogger<WebSocketManager> _logger;
|
private readonly ILogger<WebSocketManager> _logger;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
public WebSocketManager(
|
public WebSocketManager(
|
||||||
|
IAuthService authService,
|
||||||
IEnumerable<IWebSocketListener> webSocketListeners,
|
IEnumerable<IWebSocketListener> webSocketListeners,
|
||||||
ILogger<WebSocketManager> logger,
|
ILogger<WebSocketManager> logger,
|
||||||
ILoggerFactory loggerFactory)
|
ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
_webSocketListeners = webSocketListeners.ToArray();
|
_webSocketListeners = webSocketListeners.ToArray();
|
||||||
|
_authService = authService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
}
|
}
|
||||||
@@ -30,6 +33,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task WebSocketRequestHandler(HttpContext context)
|
public async Task WebSocketRequestHandler(HttpContext context)
|
||||||
{
|
{
|
||||||
|
_ = _authService.Authenticate(context.Request);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
_logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);
|
||||||
|
|||||||
@@ -249,9 +249,18 @@ namespace Emby.Server.Implementations.IO
|
|||||||
// Issue #2354 get the size of files behind symbolic links
|
// Issue #2354 get the size of files behind symbolic links
|
||||||
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
if (fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||||
{
|
{
|
||||||
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
|
try
|
||||||
{
|
{
|
||||||
result.Length = thisFileStream.Length;
|
using (Stream thisFileStream = File.OpenRead(fileInfo.FullName))
|
||||||
|
{
|
||||||
|
result.Length = thisFileStream.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
// Dangling symlinks cannot be detected before opening the file unfortunately...
|
||||||
|
Logger.LogError(ex, "Reading the file size of the symlink at {Path} failed. Marking the file as not existing.", fileInfo.FullName);
|
||||||
|
result.Exists = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,9 +591,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
|
||||||
|
|
||||||
return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
|
||||||
@@ -594,16 +601,16 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string> extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and osx the search pattern is case sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Count == 1)
|
||||||
{
|
{
|
||||||
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption));
|
return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption);
|
var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
|
||||||
|
|
||||||
if (extensions != null && extensions.Count > 0)
|
if (extensions != null && extensions.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -625,10 +632,10 @@ namespace Emby.Server.Implementations.IO
|
|||||||
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var directoryInfo = new DirectoryInfo(path);
|
var directoryInfo = new DirectoryInfo(path);
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption))
|
return ToMetadata(directoryInfo.EnumerateDirectories("*", enumerationOptions))
|
||||||
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption)));
|
.Concat(ToMetadata(directoryInfo.EnumerateFiles("*", enumerationOptions)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
private IEnumerable<FileSystemMetadata> ToMetadata(IEnumerable<FileSystemInfo> infos)
|
||||||
@@ -638,8 +645,7 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
|
||||||
return Directory.EnumerateDirectories(path, "*", searchOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
|
||||||
@@ -649,16 +655,16 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
public virtual IEnumerable<string> GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
var enumerationOptions = GetEnumerationOptions(recursive);
|
||||||
|
|
||||||
// On linux and osx the search pattern is case sensitive
|
// On linux and osx the search pattern is case sensitive
|
||||||
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
// If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
|
||||||
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
|
if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions != null && extensions.Length == 1)
|
||||||
{
|
{
|
||||||
return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption);
|
return Directory.EnumerateFiles(path, "*" + extensions[0], enumerationOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(path, "*", searchOption);
|
var files = Directory.EnumerateFiles(path, "*", enumerationOptions);
|
||||||
|
|
||||||
if (extensions != null && extensions.Length > 0)
|
if (extensions != null && extensions.Length > 0)
|
||||||
{
|
{
|
||||||
@@ -679,8 +685,18 @@ namespace Emby.Server.Implementations.IO
|
|||||||
|
|
||||||
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
|
||||||
{
|
{
|
||||||
var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));
|
||||||
return Directory.EnumerateFileSystemEntries(path, "*", searchOption);
|
}
|
||||||
|
|
||||||
|
private EnumerationOptions GetEnumerationOptions(bool recursive)
|
||||||
|
{
|
||||||
|
return new EnumerationOptions
|
||||||
|
{
|
||||||
|
RecurseSubdirectories = recursive,
|
||||||
|
IgnoreInaccessible = true,
|
||||||
|
// Don't skip any files.
|
||||||
|
AttributesToSkip = 0
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunProcess(string path, string args, string workingDirectory)
|
private static void RunProcess(string path, string args, string workingDirectory)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations
|
namespace Emby.Server.Implementations
|
||||||
@@ -9,7 +9,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --ffmpeg command line option.
|
/// Gets the value of the --ffmpeg command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string FFmpegPath { get; }
|
string? FFmpegPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --service command line option.
|
/// Gets the value of the --service command line option.
|
||||||
@@ -19,21 +19,21 @@ namespace Emby.Server.Implementations
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --package-name command line option.
|
/// Gets the value of the --package-name command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string PackageName { get; }
|
string? PackageName { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --restartpath command line option.
|
/// Gets the value of the --restartpath command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string RestartPath { get; }
|
string? RestartPath { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --restartargs command line option.
|
/// Gets the value of the --restartargs command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string RestartArgs { get; }
|
string? RestartArgs { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value of the --published-server-url command line option.
|
/// Gets the value of the --published-server-url command line option.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Uri PublishedServerUrl { get; }
|
string? PublishedServerUrl { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1240,11 +1240,20 @@ namespace Emby.Server.Implementations.Library
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetCollectionType(string path)
|
private CollectionTypeOptions? GetCollectionType(string path)
|
||||||
{
|
{
|
||||||
return _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false)
|
var files = _fileSystem.GetFilePaths(path, new[] { ".collection" }, true, false);
|
||||||
.Select(Path.GetFileNameWithoutExtension)
|
foreach (var file in files)
|
||||||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
{
|
||||||
|
// TODO: @bond use a ReadOnlySpan<char> here when Enum.TryParse supports it
|
||||||
|
// https://github.com/dotnet/runtime/issues/20008
|
||||||
|
if (Enum.TryParse<CollectionTypeOptions>(Path.GetFileNameWithoutExtension(file), true, out var res))
|
||||||
|
{
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1905,12 +1914,17 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Cannot get image index for {0}", img.Path);
|
_logger.LogWarning("Cannot get image index for {ImagePath}", img.Path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
catch (InvalidOperationException)
|
catch (Exception ex) when (ex is InvalidOperationException || ex is IOException)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Cannot fetch image from {0}", img.Path);
|
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}", img.Path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Cannot fetch image from {ImagePath}. Http status code: {HttpStatus}", img.Path, ex.StatusCode);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1923,7 +1937,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Cannot get image dimensions for {0}", image.Path);
|
_logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
|
||||||
image.Width = 0;
|
image.Width = 0;
|
||||||
image.Height = 0;
|
image.Height = 0;
|
||||||
continue;
|
continue;
|
||||||
@@ -1935,7 +1949,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Cannot compute blurhash for {0}", image.Path);
|
_logger.LogError(ex, "Cannot compute blurhash for {ImagePath}", image.Path);
|
||||||
image.BlurHash = string.Empty;
|
image.BlurHash = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1945,7 +1959,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Cannot update DateModified for {0}", image.Path);
|
_logger.LogError(ex, "Cannot update DateModified for {ImagePath}", image.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2767,6 +2781,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
public string GetPathAfterNetworkSubstitution(string path, BaseItem ownerItem)
|
||||||
{
|
{
|
||||||
|
string newPath;
|
||||||
if (ownerItem != null)
|
if (ownerItem != null)
|
||||||
{
|
{
|
||||||
var libraryOptions = GetLibraryOptions(ownerItem);
|
var libraryOptions = GetLibraryOptions(ownerItem);
|
||||||
@@ -2774,15 +2789,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
foreach (var pathInfo in libraryOptions.PathInfos)
|
foreach (var pathInfo in libraryOptions.PathInfos)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(pathInfo.Path) || string.IsNullOrWhiteSpace(pathInfo.NetworkPath))
|
if (path.TryReplaceSubPath(pathInfo.Path, pathInfo.NetworkPath, out newPath))
|
||||||
{
|
{
|
||||||
continue;
|
return newPath;
|
||||||
}
|
|
||||||
|
|
||||||
var substitutionResult = SubstitutePathInternal(path, pathInfo.Path, pathInfo.NetworkPath);
|
|
||||||
if (substitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return substitutionResult.Item1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2791,24 +2800,16 @@ namespace Emby.Server.Implementations.Library
|
|||||||
var metadataPath = _configurationManager.Configuration.MetadataPath;
|
var metadataPath = _configurationManager.Configuration.MetadataPath;
|
||||||
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
|
var metadataNetworkPath = _configurationManager.Configuration.MetadataNetworkPath;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(metadataPath) && !string.IsNullOrWhiteSpace(metadataNetworkPath))
|
if (path.TryReplaceSubPath(metadataPath, metadataNetworkPath, out newPath))
|
||||||
{
|
{
|
||||||
var metadataSubstitutionResult = SubstitutePathInternal(path, metadataPath, metadataNetworkPath);
|
return newPath;
|
||||||
if (metadataSubstitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return metadataSubstitutionResult.Item1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
|
foreach (var map in _configurationManager.Configuration.PathSubstitutions)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(map.From))
|
if (path.TryReplaceSubPath(map.From, map.To, out newPath))
|
||||||
{
|
{
|
||||||
var substitutionResult = SubstitutePathInternal(path, map.From, map.To);
|
return newPath;
|
||||||
if (substitutionResult.Item2)
|
|
||||||
{
|
|
||||||
return substitutionResult.Item1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2817,47 +2818,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
public string SubstitutePath(string path, string from, string to)
|
public string SubstitutePath(string path, string from, string to)
|
||||||
{
|
{
|
||||||
return SubstitutePathInternal(path, from, to).Item1;
|
if (path.TryReplaceSubPath(from, to, out var newPath))
|
||||||
}
|
|
||||||
|
|
||||||
private Tuple<string, bool> SubstitutePathInternal(string path, string from, string to)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(path));
|
return newPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(from))
|
return path;
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(from));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(to))
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(to));
|
|
||||||
}
|
|
||||||
|
|
||||||
from = from.Trim();
|
|
||||||
to = to.Trim();
|
|
||||||
|
|
||||||
var newPath = path.Replace(from, to, StringComparison.OrdinalIgnoreCase);
|
|
||||||
var changed = false;
|
|
||||||
|
|
||||||
if (!string.Equals(newPath, path, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (to.IndexOf('/', StringComparison.Ordinal) != -1)
|
|
||||||
{
|
|
||||||
newPath = newPath.Replace('\\', '/');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newPath = newPath.Replace('/', '\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Tuple<string, bool>(newPath, changed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetExtraTypeFromFilename(Video item)
|
private void SetExtraTypeFromFilename(Video item)
|
||||||
@@ -2914,6 +2880,12 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
|
public void UpdatePeople(BaseItem item, List<PersonInfo> people)
|
||||||
|
{
|
||||||
|
UpdatePeopleAsync(item, people, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task UpdatePeopleAsync(BaseItem item, List<PersonInfo> people, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!item.SupportsPeople)
|
if (!item.SupportsPeople)
|
||||||
{
|
{
|
||||||
@@ -2921,6 +2893,8 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
|
|
||||||
_itemRepository.UpdatePeople(item.Id, people);
|
_itemRepository.UpdatePeople(item.Id, people);
|
||||||
|
|
||||||
|
await SavePeopleMetadataAsync(people, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
|
public async Task<ItemImageInfo> ConvertImageToLocal(BaseItem item, ItemImageInfo image, int imageIndex)
|
||||||
@@ -2956,7 +2930,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddVirtualFolder(string name, string collectionType, LibraryOptions options, bool refreshLibrary)
|
public async Task AddVirtualFolder(string name, CollectionTypeOptions? collectionType, LibraryOptions options, bool refreshLibrary)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
@@ -2990,9 +2964,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(virtualFolderPath);
|
Directory.CreateDirectory(virtualFolderPath);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(collectionType))
|
if (collectionType != null)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(virtualFolderPath, collectionType + ".collection");
|
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
|
||||||
|
|
||||||
File.WriteAllBytes(path, Array.Empty<byte>());
|
File.WriteAllBytes(path, Array.Empty<byte>());
|
||||||
}
|
}
|
||||||
@@ -3024,6 +2998,58 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var personsToSave = new List<BaseItem>();
|
||||||
|
|
||||||
|
foreach (var person in people)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var itemUpdateType = ItemUpdateType.MetadataDownload;
|
||||||
|
var saveEntity = false;
|
||||||
|
var personEntity = GetPerson(person.Name);
|
||||||
|
|
||||||
|
// if PresentationUniqueKey is empty it's likely a new item.
|
||||||
|
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
||||||
|
{
|
||||||
|
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||||
|
saveEntity = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var id in person.ProviderIds)
|
||||||
|
{
|
||||||
|
if (!string.Equals(personEntity.GetProviderId(id.Key), id.Value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
personEntity.SetProviderId(id.Key, id.Value);
|
||||||
|
saveEntity = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(person.ImageUrl) && !personEntity.HasImage(ImageType.Primary))
|
||||||
|
{
|
||||||
|
personEntity.SetImage(
|
||||||
|
new ItemImageInfo
|
||||||
|
{
|
||||||
|
Path = person.ImageUrl,
|
||||||
|
Type = ImageType.Primary
|
||||||
|
},
|
||||||
|
0);
|
||||||
|
|
||||||
|
saveEntity = true;
|
||||||
|
itemUpdateType = ItemUpdateType.ImageUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveEntity)
|
||||||
|
{
|
||||||
|
personsToSave.Add(personEntity);
|
||||||
|
await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateItems(personsToSave, null, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
private void StartScanInBackground()
|
private void StartScanInBackground()
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SortMediaSources(list).Where(i => i.Type != MediaSourceType.Placeholder).ToList();
|
return SortMediaSources(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MediaProtocol GetPathProtocol(string path)
|
public MediaProtocol GetPathProtocol(string path)
|
||||||
@@ -437,7 +437,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
private static List<MediaSourceInfo> SortMediaSources(IEnumerable<MediaSourceInfo> sources)
|
||||||
{
|
{
|
||||||
return sources.OrderBy(i =>
|
return sources.OrderBy(i =>
|
||||||
{
|
{
|
||||||
@@ -452,8 +452,9 @@ namespace Emby.Server.Implementations.Library
|
|||||||
{
|
{
|
||||||
var stream = i.VideoStream;
|
var stream = i.VideoStream;
|
||||||
|
|
||||||
return stream == null || stream.Width == null ? 0 : stream.Width.Value;
|
return stream?.Width ?? 0;
|
||||||
})
|
})
|
||||||
|
.Where(i => i.Type != MediaSourceType.Placeholder)
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Library
|
namespace Emby.Server.Implementations.Library
|
||||||
@@ -47,5 +49,65 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replaces a sub path with another sub path and normalizes the final path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The original path.</param>
|
||||||
|
/// <param name="subPath">The original sub path.</param>
|
||||||
|
/// <param name="newSubPath">The new sub path.</param>
|
||||||
|
/// <param name="newPath">The result of the sub path replacement</param>
|
||||||
|
/// <returns>The path after replacing the sub path.</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="path" />, <paramref name="newSubPath" /> or <paramref name="newSubPath" /> is empty.</exception>
|
||||||
|
public static bool TryReplaceSubPath(this string path, string subPath, string newSubPath, [NotNullWhen(true)] out string? newPath)
|
||||||
|
{
|
||||||
|
newPath = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(subPath) || string.IsNullOrEmpty(newSubPath) || subPath.Length > path.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char oldDirectorySeparatorChar;
|
||||||
|
char newDirectorySeparatorChar;
|
||||||
|
// True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
|
||||||
|
// The reasoning behind this is that a forward slash likely means it's a Linux path and
|
||||||
|
// so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
|
||||||
|
if (newSubPath.Contains('/', StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
oldDirectorySeparatorChar = '\\';
|
||||||
|
newDirectorySeparatorChar = '/';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oldDirectorySeparatorChar = '/';
|
||||||
|
newDirectorySeparatorChar = '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||||
|
subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
|
||||||
|
|
||||||
|
// We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
|
||||||
|
// when the sub path matches a similar but in-complete subpath
|
||||||
|
var oldSubPathEndsWithSeparator = subPath[^1] == newDirectorySeparatorChar;
|
||||||
|
if (!path.StartsWith(subPath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.Length > subPath.Length
|
||||||
|
&& !oldSubPathEndsWithSeparator
|
||||||
|
&& path[subPath.Length] != newDirectorySeparatorChar)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSubPathTrimmed = newSubPath.AsSpan().TrimEnd(newDirectorySeparatorChar);
|
||||||
|
// Ensure that the path with the old subpath removed starts with a leading dir separator
|
||||||
|
int idx = oldSubPathEndsWithSeparator ? subPath.Length - 1 : subPath.Length;
|
||||||
|
newPath = string.Concat(newSubPathTrimmed, path.AsSpan(idx));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
public override ResolverPriority Priority => ResolverPriority.Fifth;
|
||||||
|
|
||||||
public MultiItemResolverResult ResolveMultiple(
|
public MultiItemResolverResult ResolveMultiple(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
@@ -201,6 +201,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolvedItem.Files.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var firstMedia = resolvedItem.Files[0];
|
var firstMedia = resolvedItem.Files[0];
|
||||||
|
|
||||||
var libraryItem = new T
|
var libraryItem = new T
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Second;
|
public override ResolverPriority Priority => ResolverPriority.Third;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the specified args.
|
/// Resolves the specified args.
|
||||||
|
|||||||
@@ -79,11 +79,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||||||
return new MusicArtist();
|
return new MusicArtist();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_config.Configuration.EnableSimpleArtistDetection)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoid mis-identifying top folders
|
// Avoid mis-identifying top folders
|
||||||
if (args.Parent.IsRoot)
|
if (args.Parent.IsRoot)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="args">The args.</param>
|
/// <param name="args">The args.</param>
|
||||||
/// <returns>`0.</returns>
|
/// <returns>`0.</returns>
|
||||||
protected override T Resolve(ItemResolveArgs args)
|
public override T Resolve(ItemResolveArgs args)
|
||||||
{
|
{
|
||||||
return ResolveVideo<T>(args, false);
|
return ResolveVideo<T>(args, false);
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
/// <param name="args">The args.</param>
|
/// <param name="args">The args.</param>
|
||||||
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
|
/// <param name="parseName">if set to <c>true</c> [parse name].</param>
|
||||||
/// <returns>``0.</returns>
|
/// <returns>``0.</returns>
|
||||||
protected TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
|
||||||
where TVideoType : Video, new()
|
where TVideoType : Video, new()
|
||||||
{
|
{
|
||||||
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||||||
{
|
{
|
||||||
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
private readonly string[] _validExtensions = { ".azw", ".azw3", ".cb7", ".cbr", ".cbt", ".cbz", ".epub", ".mobi", ".pdf" };
|
||||||
|
|
||||||
protected override Book Resolve(ItemResolveArgs args)
|
public override Book Resolve(ItemResolveArgs args)
|
||||||
{
|
{
|
||||||
var collectionType = args.GetCollectionType();
|
var collectionType = args.GetCollectionType();
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
/// Gets the priority.
|
/// Gets the priority.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The priority.</value>
|
/// <value>The priority.</value>
|
||||||
public override ResolverPriority Priority => ResolverPriority.Third;
|
public override ResolverPriority Priority => ResolverPriority.Fourth;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public MultiItemResolverResult ResolveMultiple(
|
public MultiItemResolverResult ResolveMultiple(
|
||||||
@@ -69,6 +69,110 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the specified args.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">The args.</param>
|
||||||
|
/// <returns>Video.</returns>
|
||||||
|
public override Video Resolve(ItemResolveArgs args)
|
||||||
|
{
|
||||||
|
var collectionType = args.GetCollectionType();
|
||||||
|
|
||||||
|
// Find movies with their own folders
|
||||||
|
if (args.IsDirectory)
|
||||||
|
{
|
||||||
|
if (IsInvalid(args.Parent, collectionType))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = args.FileSystemChildren
|
||||||
|
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(collectionType))
|
||||||
|
{
|
||||||
|
// Owned items will be caught by the plain video resolver
|
||||||
|
if (args.Parent == null)
|
||||||
|
{
|
||||||
|
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.HasParent<Series>())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle owned items
|
||||||
|
if (args.Parent == null)
|
||||||
|
{
|
||||||
|
return base.Resolve(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInvalid(args.Parent, collectionType))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Video item = null;
|
||||||
|
|
||||||
|
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
item = ResolveVideo<MusicVideo>(args, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// To find a movie file, the collection type must be movies or boxsets
|
||||||
|
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
item = ResolveVideo<Movie>(args, true);
|
||||||
|
}
|
||||||
|
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
item = ResolveVideo<Video>(args, false);
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrEmpty(collectionType))
|
||||||
|
{
|
||||||
|
if (args.HasParent<Series>())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
item = ResolveVideo<Video>(args, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item != null)
|
||||||
|
{
|
||||||
|
item.IsInMixedFolder = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
private MultiItemResolverResult ResolveMultipleInternal(
|
private MultiItemResolverResult ResolveMultipleInternal(
|
||||||
Folder parent,
|
Folder parent,
|
||||||
List<FileSystemMetadata> files,
|
List<FileSystemMetadata> files,
|
||||||
@@ -216,110 +320,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||||||
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves the specified args.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">The args.</param>
|
|
||||||
/// <returns>Video.</returns>
|
|
||||||
protected override Video Resolve(ItemResolveArgs args)
|
|
||||||
{
|
|
||||||
var collectionType = args.GetCollectionType();
|
|
||||||
|
|
||||||
// Find movies with their own folders
|
|
||||||
if (args.IsDirectory)
|
|
||||||
{
|
|
||||||
if (IsInvalid(args.Parent, collectionType))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = args.FileSystemChildren
|
|
||||||
.Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(collectionType))
|
|
||||||
{
|
|
||||||
// Owned items will be caught by the plain video resolver
|
|
||||||
if (args.Parent == null)
|
|
||||||
{
|
|
||||||
// return FindMovie<Video>(args.Path, args.Parent, files, args.DirectoryService, collectionType);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (args.HasParent<Series>())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle owned items
|
|
||||||
if (args.Parent == null)
|
|
||||||
{
|
|
||||||
return base.Resolve(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInvalid(args.Parent, collectionType))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Video item = null;
|
|
||||||
|
|
||||||
if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item = ResolveVideo<MusicVideo>(args, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// To find a movie file, the collection type must be movies or boxsets
|
|
||||||
else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item = ResolveVideo<Movie>(args, true);
|
|
||||||
}
|
|
||||||
else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item = ResolveVideo<Video>(args, false);
|
|
||||||
}
|
|
||||||
else if (string.IsNullOrEmpty(collectionType))
|
|
||||||
{
|
|
||||||
if (args.HasParent<Series>())
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
item = ResolveVideo<Video>(args, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
item.IsInMixedFolder = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the initial item values.
|
/// Sets the initial item values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It's a directory-based playlist if the directory contains a playlist file
|
// It's a directory-based playlist if the directory contains a playlist file
|
||||||
var filePaths = Directory.EnumerateFiles(args.Path);
|
var filePaths = Directory.EnumerateFiles(args.Path, "*", new EnumerationOptions { IgnoreInaccessible = true });
|
||||||
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
|
if (filePaths.Any(f => f.EndsWith(PlaylistXmlSaver.DefaultPlaylistFilename, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
return new Playlist
|
return new Playlist
|
||||||
@@ -63,7 +63,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
{
|
{
|
||||||
Path = args.Path,
|
Path = args.Path,
|
||||||
Name = Path.GetFileNameWithoutExtension(args.Path),
|
Name = Path.GetFileNameWithoutExtension(args.Path),
|
||||||
IsInMixedFolder = true
|
IsInMixedFolder = true,
|
||||||
|
PlaylistMediaType = MediaType.Audio
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
using MediaBrowser.Controller.Entities.TV;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
@@ -11,12 +12,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class EpisodeResolver : BaseVideoResolver<Episode>
|
public class EpisodeResolver : BaseVideoResolver<Episode>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="libraryManager">The library manager.</param>
|
||||||
|
public EpisodeResolver(ILibraryManager libraryManager)
|
||||||
|
: base(libraryManager)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the specified args.
|
/// Resolves the specified args.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="args">The args.</param>
|
/// <param name="args">The args.</param>
|
||||||
/// <returns>Episode.</returns>
|
/// <returns>Episode.</returns>
|
||||||
protected override Episode Resolve(ItemResolveArgs args)
|
public override Episode Resolve(ItemResolveArgs args)
|
||||||
{
|
{
|
||||||
var parent = args.Parent;
|
var parent = args.Parent;
|
||||||
|
|
||||||
@@ -34,11 +44,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
season = parent.GetParents().OfType<Season>().FirstOrDefault();
|
season = parent.GetParents().OfType<Season>().FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the parent is a Season or Series, then this is an Episode if the VideoResolver returns something
|
// If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
|
||||||
// Also handle flat tv folders
|
// Also handle flat tv folders
|
||||||
if (season != null ||
|
if ((season != null ||
|
||||||
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
|
||||||
args.HasParent<Series>())
|
args.HasParent<Series>())
|
||||||
|
&& (parent is Series || !BaseItem.AllExtrasTypesFolderNames.Contains(parent.Name, StringComparer.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
var episode = ResolveVideo<Episode>(args, false);
|
var episode = ResolveVideo<Episode>(args, false);
|
||||||
|
|
||||||
@@ -74,14 +85,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
public EpisodeResolver(ILibraryManager libraryManager)
|
|
||||||
: base(libraryManager)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,15 +248,15 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
|
else if (positionTicks > 0 && hasRuntime && item is AudioBook)
|
||||||
{
|
{
|
||||||
var minIn = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
var playbackPositionInMinutes = TimeSpan.FromTicks(positionTicks).TotalMinutes;
|
||||||
var minOut = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
var remainingTimeInMinutes = TimeSpan.FromTicks(runtimeTicks - positionTicks).TotalMinutes;
|
||||||
|
|
||||||
if (minIn > _config.Configuration.MinAudiobookResume)
|
if (playbackPositionInMinutes < _config.Configuration.MinAudiobookResume)
|
||||||
{
|
{
|
||||||
// ignore progress during the beginning
|
// ignore progress during the beginning
|
||||||
positionTicks = 0;
|
positionTicks = 0;
|
||||||
}
|
}
|
||||||
else if (minOut < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
else if (remainingTimeInMinutes < _config.Configuration.MaxAudiobookResume || positionTicks >= runtimeTicks)
|
||||||
{
|
{
|
||||||
// mark as completed close to the end
|
// mark as completed close to the end
|
||||||
positionTicks = 0;
|
positionTicks = 0;
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||||
|
|
||||||
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
onStarted();
|
onStarted();
|
||||||
|
|
||||||
@@ -70,7 +71,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
Directory.CreateDirectory(Path.GetDirectoryName(targetFile));
|
||||||
|
|
||||||
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.Read);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
await using var output = new FileStream(targetFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
|
||||||
onStarted();
|
onStarted();
|
||||||
|
|
||||||
|
|||||||
@@ -1860,7 +1860,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@@ -1924,7 +1925,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.Read))
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
using (var stream = new FileStream(nfoPath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@@ -2608,7 +2610,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Locations = new string[] { customPath },
|
Locations = new string[] { customPath },
|
||||||
Name = "Recorded Movies",
|
Name = "Recorded Movies",
|
||||||
CollectionType = CollectionType.Movies
|
CollectionType = CollectionTypeOptions.Movies
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2619,7 +2621,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||||||
{
|
{
|
||||||
Locations = new string[] { customPath },
|
Locations = new string[] { customPath },
|
||||||
Name = "Recorded Shows",
|
Name = "Recorded Shows",
|
||||||
CollectionType = CollectionType.TvShows
|
CollectionType = CollectionTypeOptions.TvShows
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ chb|||Chibcha|chibcha
|
|||||||
che||ce|Chechen|tchétchène
|
che||ce|Chechen|tchétchène
|
||||||
chg|||Chagatai|djaghataï
|
chg|||Chagatai|djaghataï
|
||||||
chi|zho|zh|Chinese|chinois
|
chi|zho|zh|Chinese|chinois
|
||||||
|
chi|zho|zh-tw|Chinese; Traditional|chinois
|
||||||
|
chi|zho|zh-hk|Chinese; Hong Kong|chinois
|
||||||
chk|||Chuukese|chuuk
|
chk|||Chuukese|chuuk
|
||||||
chm|||Mari|mari
|
chm|||Mari|mari
|
||||||
chn|||Chinook jargon|chinook, jargon
|
chn|||Chinook jargon|chinook, jargon
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ namespace Emby.Server.Implementations.MediaEncoder
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.VideoType == VideoType.Dvd)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (video.IsShortcut)
|
if (video.IsShortcut)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -11,9 +14,11 @@ using MediaBrowser.Common;
|
|||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Json;
|
using MediaBrowser.Common.Json;
|
||||||
using MediaBrowser.Common.Json.Converters;
|
using MediaBrowser.Common.Json.Converters;
|
||||||
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Plugins;
|
using MediaBrowser.Model.Plugins;
|
||||||
|
using MediaBrowser.Model.Updates;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -33,6 +38,21 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
private readonly IList<LocalPlugin> _plugins;
|
private readonly IList<LocalPlugin> _plugins;
|
||||||
private readonly Version _minimumVersion;
|
private readonly Version _minimumVersion;
|
||||||
|
|
||||||
|
private IHttpClientFactory? _httpClientFactory;
|
||||||
|
|
||||||
|
private IHttpClientFactory HttpClientFactory
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_httpClientFactory == null)
|
||||||
|
{
|
||||||
|
_httpClientFactory = _appHost.Resolve<IHttpClientFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _httpClientFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
/// Initializes a new instance of the <see cref="PluginManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -112,8 +132,6 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
{
|
{
|
||||||
assembly = Assembly.LoadFrom(file);
|
assembly = Assembly.LoadFrom(file);
|
||||||
|
|
||||||
// This force loads all reference dll's that the plugin uses in the try..catch block.
|
|
||||||
// Removing this will cause JF to bomb out if referenced dll's cause issues.
|
|
||||||
assembly.GetExportedTypes();
|
assembly.GetExportedTypes();
|
||||||
}
|
}
|
||||||
catch (FileLoadException ex)
|
catch (FileLoadException ex)
|
||||||
@@ -122,6 +140,20 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
catch (TypeLoadException ex) // Undocumented exception
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
|
||||||
|
ChangePluginState(plugin, PluginStatus.NotSupported);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
#pragma warning disable CA1031 // Do not catch general exception types
|
||||||
|
catch (Exception ex)
|
||||||
|
#pragma warning restore CA1031 // Do not catch general exception types
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin.", file);
|
||||||
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
|
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
|
||||||
yield return assembly;
|
yield return assembly;
|
||||||
@@ -320,32 +352,74 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc/>
|
||||||
/// Saves the manifest back to disk.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
|
|
||||||
/// <param name="path">The path where to save the manifest.</param>
|
|
||||||
/// <returns>True if successful.</returns>
|
|
||||||
public bool SaveManifest(PluginManifest manifest, string path)
|
public bool SaveManifest(PluginManifest manifest, string path)
|
||||||
{
|
{
|
||||||
if (manifest == null)
|
try
|
||||||
|
{
|
||||||
|
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
||||||
|
File.WriteAllText(Path.Combine(path, "meta.json"), data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentException e)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(e, "Unable to save plugin manifest due to invalid value. {Path}", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<bool> GenerateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status)
|
||||||
|
{
|
||||||
|
if (packageInfo == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var versionInfo = packageInfo.Versions.First(v => v.Version == version.ToString());
|
||||||
|
var imagePath = string.Empty;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(packageInfo.ImageUrl))
|
||||||
{
|
{
|
||||||
var data = JsonSerializer.Serialize(manifest, _jsonOptions);
|
var url = new Uri(packageInfo.ImageUrl);
|
||||||
File.WriteAllText(Path.Combine(path, "meta.json"), data, Encoding.UTF8);
|
imagePath = Path.Join(path, url.Segments[^1]);
|
||||||
return true;
|
|
||||||
|
await using var fileStream = File.OpenWrite(imagePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var downloadStream = await HttpClientFactory
|
||||||
|
.CreateClient(NamedClient.Default)
|
||||||
|
.GetStreamAsync(url)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
await downloadStream.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
|
||||||
|
imagePath = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#pragma warning disable CA1031 // Do not catch general exception types
|
|
||||||
catch (Exception e)
|
var manifest = new PluginManifest
|
||||||
#pragma warning restore CA1031 // Do not catch general exception types
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "Unable to save plugin manifest. {Path}", path);
|
Category = packageInfo.Category,
|
||||||
return false;
|
Changelog = versionInfo.Changelog ?? string.Empty,
|
||||||
}
|
Description = packageInfo.Description,
|
||||||
|
Id = new Guid(packageInfo.Id),
|
||||||
|
Name = packageInfo.Name,
|
||||||
|
Overview = packageInfo.Overview,
|
||||||
|
Owner = packageInfo.Owner,
|
||||||
|
TargetAbi = versionInfo.TargetAbi ?? string.Empty,
|
||||||
|
Timestamp = string.IsNullOrEmpty(versionInfo.Timestamp) ? DateTime.MinValue : DateTime.Parse(versionInfo.Timestamp, CultureInfo.InvariantCulture),
|
||||||
|
Version = versionInfo.Version,
|
||||||
|
Status = status == PluginStatus.Disabled ? PluginStatus.Disabled : PluginStatus.Active, // Keep disabled state.
|
||||||
|
AutoUpdate = true,
|
||||||
|
ImagePath = imagePath
|
||||||
|
};
|
||||||
|
|
||||||
|
return SaveManifest(manifest, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -374,7 +448,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
|
private LocalPlugin? GetPluginByAssembly(Assembly assembly)
|
||||||
{
|
{
|
||||||
// Find which plugin it is by the path.
|
// Find which plugin it is by the path.
|
||||||
return _plugins.FirstOrDefault(p => string.Equals(p.Path, Path.GetDirectoryName(assembly.Location), StringComparison.Ordinal));
|
return _plugins.FirstOrDefault(p => p.DllFiles.Contains(assembly.Location, StringComparer.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -398,7 +472,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
if (plugin == null)
|
if (plugin == null)
|
||||||
{
|
{
|
||||||
// Create a dummy record for the providers.
|
// Create a dummy record for the providers.
|
||||||
// TODO: remove this code, if all provided have been released as separate plugins.
|
// TODO: remove this code once all provided have been released as separate plugins.
|
||||||
plugin = new LocalPlugin(
|
plugin = new LocalPlugin(
|
||||||
instance.AssemblyFilePath,
|
instance.AssemblyFilePath,
|
||||||
true,
|
true,
|
||||||
@@ -421,15 +495,17 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
{
|
{
|
||||||
plugin.Instance = instance;
|
plugin.Instance = instance;
|
||||||
var manifest = plugin.Manifest;
|
var manifest = plugin.Manifest;
|
||||||
var pluginStr = plugin.Instance.Version.ToString();
|
var pluginStr = instance.Version.ToString();
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal))
|
if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal)
|
||||||
|
|| manifest.Id != instance.Id)
|
||||||
{
|
{
|
||||||
// If a plugin without a manifest failed to load due to an external issue (eg config),
|
// If a plugin without a manifest failed to load due to an external issue (eg config),
|
||||||
// this updates the manifest to the actual plugin values.
|
// this updates the manifest to the actual plugin values.
|
||||||
manifest.Version = pluginStr;
|
manifest.Version = pluginStr;
|
||||||
manifest.Name = plugin.Instance.Name;
|
manifest.Name = plugin.Instance.Name;
|
||||||
manifest.Description = plugin.Instance.Description;
|
manifest.Description = plugin.Instance.Description;
|
||||||
|
manifest.Id = plugin.Instance.Id;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,7 +635,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
|
// Auto-create a plugin manifest, so we can disable it, if it fails to load.
|
||||||
manifest = new PluginManifest
|
manifest = new PluginManifest
|
||||||
{
|
{
|
||||||
Status = PluginStatus.Restart,
|
Status = PluginStatus.Active,
|
||||||
Name = metafile,
|
Name = metafile,
|
||||||
AutoUpdate = false,
|
AutoUpdate = false,
|
||||||
Id = metafile.GetMD5(),
|
Id = metafile.GetMD5(),
|
||||||
|
|||||||
@@ -80,10 +80,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||||||
// Delete log files more than n days old
|
// Delete log files more than n days old
|
||||||
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays);
|
||||||
|
|
||||||
// Only delete the .txt log files, the *.log files created by serilog get managed by itself
|
// Only delete files that serilog doesn't manage (anything that doesn't start with 'log_'
|
||||||
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, new[] { ".txt" }, true, true)
|
var filesToDelete = _fileSystem.GetFiles(_configurationManager.CommonApplicationPaths.LogDirectoryPath, true)
|
||||||
.Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
.Where(f => !f.Name.StartsWith("log_", StringComparison.Ordinal)
|
||||||
.ToList();
|
&& _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
var index = 0;
|
var index = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1310,7 +1310,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return SendMessageToSession(session, SessionMessageType.PlayState, command, cancellationToken);
|
return SendMessageToSession(session, SessionMessageType.Playstate, command, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
|
private static void AssertCanControl(SessionInfo session, SessionInfo controllingSession)
|
||||||
@@ -1456,7 +1456,12 @@ namespace Emby.Server.Implementations.Session
|
|||||||
throw new SecurityException("Unknown quick connect token");
|
throw new SecurityException("Unknown quick connect token");
|
||||||
}
|
}
|
||||||
|
|
||||||
request.UserId = result.Items[0].UserId;
|
var info = result.Items[0];
|
||||||
|
request.UserId = info.UserId;
|
||||||
|
|
||||||
|
// There's no need to keep the quick connect token in the database, as AuthenticateNewSessionInternal() issues a long lived token.
|
||||||
|
_authRepo.Delete(info);
|
||||||
|
|
||||||
return AuthenticateNewSessionInternal(request, false);
|
return AuthenticateNewSessionInternal(request, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1538,23 +1543,26 @@ namespace Emby.Server.Implementations.Session
|
|||||||
Limit = 1
|
Limit = 1
|
||||||
}).Items.FirstOrDefault();
|
}).Items.FirstOrDefault();
|
||||||
|
|
||||||
var allExistingForDevice = _authRepo.Get(
|
if (!string.IsNullOrEmpty(deviceId))
|
||||||
new AuthenticationInfoQuery
|
|
||||||
{
|
|
||||||
DeviceId = deviceId
|
|
||||||
}).Items;
|
|
||||||
|
|
||||||
foreach (var auth in allExistingForDevice)
|
|
||||||
{
|
{
|
||||||
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
|
var allExistingForDevice = _authRepo.Get(
|
||||||
|
new AuthenticationInfoQuery
|
||||||
|
{
|
||||||
|
DeviceId = deviceId
|
||||||
|
}).Items;
|
||||||
|
|
||||||
|
foreach (var auth in allExistingForDevice)
|
||||||
{
|
{
|
||||||
try
|
if (existing == null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Logout(auth);
|
try
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
Logout(auth);
|
||||||
{
|
}
|
||||||
_logger.LogError(ex, "Error while logging out.");
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error while logging out.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,11 +131,11 @@ namespace Emby.Server.Implementations.Sorting
|
|||||||
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
|
return GetSpecialCompareValue(x).CompareTo(GetSpecialCompareValue(y));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetSpecialCompareValue(Episode item)
|
private static long GetSpecialCompareValue(Episode item)
|
||||||
{
|
{
|
||||||
// First sort by season number
|
// First sort by season number
|
||||||
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
|
// Since there are three sort orders, pad with 9 digits (3 for each, figure 1000 episode buffer should be enough)
|
||||||
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000;
|
var val = (item.AirsAfterSeasonNumber ?? item.AirsBeforeSeasonNumber ?? 0) * 1000000000L;
|
||||||
|
|
||||||
// Second sort order is if it airs after the season
|
// Second sort order is if it airs after the season
|
||||||
if (item.AirsAfterSeasonNumber.HasValue)
|
if (item.AirsAfterSeasonNumber.HasValue)
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
_logger = loggerFactory.CreateLogger<SyncPlayManager>();
|
||||||
_sessionManager.SessionControllerConnected += OnSessionControllerConnected;
|
_sessionManager.SessionEnded += OnSessionEnded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -352,18 +352,18 @@ namespace Emby.Server.Implementations.SyncPlay
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sessionManager.SessionControllerConnected -= OnSessionControllerConnected;
|
_sessionManager.SessionEnded -= OnSessionEnded;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSessionControllerConnected(object sender, SessionEventArgs e)
|
private void OnSessionEnded(object sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
var session = e.SessionInfo;
|
var session = e.SessionInfo;
|
||||||
|
|
||||||
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
|
if (_sessionToGroupMap.TryGetValue(session.Id, out var group))
|
||||||
{
|
{
|
||||||
var request = new JoinGroupRequest(group.GroupId);
|
var leaveGroupRequest = new LeaveGroupRequest();
|
||||||
JoinGroup(session, request, CancellationToken.None);
|
LeaveGroup(session, leaveGroupRequest, CancellationToken.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -11,7 +10,6 @@ using MediaBrowser.Controller.Dto;
|
|||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||||
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
using Series = MediaBrowser.Controller.Entities.TV.Series;
|
||||||
@@ -23,12 +21,14 @@ namespace Emby.Server.Implementations.TV
|
|||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
|
||||||
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager)
|
public TVSeriesManager(IUserManager userManager, IUserDataManager userDataManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_libraryManager = libraryManager;
|
_libraryManager = libraryManager;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
|
public QueryResult<BaseItem> GetNextUp(NextUpQuery request, DtoOptions dtoOptions)
|
||||||
@@ -143,10 +143,31 @@ namespace Emby.Server.Implementations.TV
|
|||||||
var allNextUp = seriesKeys
|
var allNextUp = seriesKeys
|
||||||
.Select(i => GetNextUp(i, currentUser, dtoOptions));
|
.Select(i => GetNextUp(i, currentUser, dtoOptions));
|
||||||
|
|
||||||
|
// If viewing all next up for all series, remove first episodes
|
||||||
|
// But if that returns empty, keep those first episodes (avoid completely empty view)
|
||||||
|
var alwaysEnableFirstEpisode = !string.IsNullOrEmpty(request.SeriesId);
|
||||||
|
var anyFound = false;
|
||||||
|
|
||||||
return allNextUp
|
return allNextUp
|
||||||
.Where(i =>
|
.Where(i =>
|
||||||
{
|
{
|
||||||
return i.Item1 != DateTime.MinValue;
|
if (request.DisableFirstEpisode)
|
||||||
|
{
|
||||||
|
return i.Item1 != DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alwaysEnableFirstEpisode || i.Item1 != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
anyFound = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyFound && i.Item1 == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
})
|
})
|
||||||
.Select(i => i.Item2())
|
.Select(i => i.Item2())
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
@@ -179,13 +200,10 @@ namespace Emby.Server.Implementations.TV
|
|||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
DtoOptions = new DtoOptions
|
DtoOptions = new DtoOptions
|
||||||
{
|
{
|
||||||
Fields = new ItemFields[]
|
Fields = new[] { ItemFields.SortName },
|
||||||
{
|
|
||||||
ItemFields.SortName
|
|
||||||
},
|
|
||||||
EnableImages = false
|
EnableImages = false
|
||||||
}
|
}
|
||||||
}).FirstOrDefault();
|
}).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
Func<Episode> getEpisode = () =>
|
Func<Episode> getEpisode = () =>
|
||||||
{
|
{
|
||||||
@@ -203,6 +221,43 @@ namespace Emby.Server.Implementations.TV
|
|||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
}).Cast<Episode>().FirstOrDefault();
|
}).Cast<Episode>().FirstOrDefault();
|
||||||
|
|
||||||
|
if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
|
||||||
|
{
|
||||||
|
var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||||
|
{
|
||||||
|
AncestorWithPresentationUniqueKey = null,
|
||||||
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
|
ParentIndexNumber = 0,
|
||||||
|
IncludeItemTypes = new[] { nameof(Episode) },
|
||||||
|
IsPlayed = false,
|
||||||
|
IsVirtualItem = false,
|
||||||
|
DtoOptions = dtoOptions
|
||||||
|
})
|
||||||
|
.Cast<Episode>()
|
||||||
|
.Where(episode => episode.AirsBeforeSeasonNumber != null || episode.AirsAfterSeasonNumber != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (lastWatchedEpisode != null)
|
||||||
|
{
|
||||||
|
// Last watched episode is added, because there could be specials that aired before the last watched episode
|
||||||
|
consideredEpisodes.Add(lastWatchedEpisode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextEpisode != null)
|
||||||
|
{
|
||||||
|
consideredEpisodes.Add(nextEpisode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) })
|
||||||
|
.Cast<Episode>();
|
||||||
|
if (lastWatchedEpisode != null)
|
||||||
|
{
|
||||||
|
sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
if (nextEpisode != null)
|
if (nextEpisode != null)
|
||||||
{
|
{
|
||||||
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Events;
|
using MediaBrowser.Controller.Events;
|
||||||
using MediaBrowser.Controller.Events.Updates;
|
using MediaBrowser.Controller.Events.Updates;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
|
using MediaBrowser.Model.Plugins;
|
||||||
using MediaBrowser.Model.Updates;
|
using MediaBrowser.Model.Updates;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -192,17 +193,12 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
var version = package.Versions[i];
|
var version = package.Versions[i];
|
||||||
|
|
||||||
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
|
var plugin = _pluginManager.GetPlugin(packageGuid, version.VersionNumber);
|
||||||
// Update the manifests, if anything changes.
|
|
||||||
if (plugin != null)
|
if (plugin != null)
|
||||||
{
|
{
|
||||||
if (!string.Equals(plugin.Manifest.TargetAbi, version.TargetAbi, StringComparison.Ordinal))
|
await _pluginManager.GenerateManifest(package, version.VersionNumber, plugin.Path, plugin.Manifest.Status).ConfigureAwait(false);
|
||||||
{
|
|
||||||
plugin.Manifest.TargetAbi = version.TargetAbi ?? string.Empty;
|
|
||||||
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove versions with a target abi that is greater then the current application version.
|
// Remove versions with a target ABI greater then the current application version.
|
||||||
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
|
if (Version.TryParse(version.TargetAbi, out var targetAbi) && _applicationHost.ApplicationVersion < targetAbi)
|
||||||
{
|
{
|
||||||
package.Versions.RemoveAt(i);
|
package.Versions.RemoveAt(i);
|
||||||
@@ -294,7 +290,8 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
Name = package.Name,
|
Name = package.Name,
|
||||||
Version = v.VersionNumber,
|
Version = v.VersionNumber,
|
||||||
SourceUrl = v.SourceUrl,
|
SourceUrl = v.SourceUrl,
|
||||||
Checksum = v.Checksum
|
Checksum = v.Checksum,
|
||||||
|
PackageInfo = package
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +501,8 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
var plugins = _pluginManager.Plugins;
|
var plugins = _pluginManager.Plugins;
|
||||||
foreach (var plugin in plugins)
|
foreach (var plugin in plugins)
|
||||||
{
|
{
|
||||||
if (plugin.Manifest?.AutoUpdate == false)
|
// Don't auto update when plugin marked not to, or when it's disabled.
|
||||||
|
if (plugin.Manifest?.AutoUpdate == false || plugin.Manifest?.Status == PluginStatus.Disabled)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -519,7 +517,7 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PerformPackageInstallation(InstallationInfo package, CancellationToken cancellationToken)
|
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(package.SourceUrl);
|
var extension = Path.GetExtension(package.SourceUrl);
|
||||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -571,24 +569,16 @@ namespace Emby.Server.Implementations.Updates
|
|||||||
|
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
_zipClient.ExtractAllFromZip(stream, targetDir, true);
|
_zipClient.ExtractAllFromZip(stream, targetDir, true);
|
||||||
|
await _pluginManager.GenerateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
|
||||||
_pluginManager.ImportPluginFrom(targetDir);
|
_pluginManager.ImportPluginFrom(targetDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
private async Task<bool> InstallPackageInternal(InstallationInfo package, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Set last update time if we were installed before
|
|
||||||
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
|
LocalPlugin? plugin = _pluginManager.Plugins.FirstOrDefault(p => p.Id.Equals(package.Id) && p.Version.Equals(package.Version))
|
||||||
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
|
?? _pluginManager.Plugins.FirstOrDefault(p => p.Name.Equals(package.Name, StringComparison.OrdinalIgnoreCase) && p.Version.Equals(package.Version));
|
||||||
if (plugin != null)
|
|
||||||
{
|
|
||||||
plugin.Manifest.Timestamp = DateTime.UtcNow;
|
|
||||||
_pluginManager.SaveManifest(plugin.Manifest, plugin.Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do the install
|
await PerformPackageInstallation(package, plugin?.Manifest.Status ?? PluginStatus.Active, cancellationToken).ConfigureAwait(false);
|
||||||
await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Do plugin-specific processing
|
|
||||||
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
_logger.LogInformation(plugin == null ? "New plugin installed: {PluginName} {PluginVersion}" : "Plugin updated: {PluginName} {PluginVersion}", package.Name, package.Version);
|
||||||
|
|
||||||
return plugin != null;
|
return plugin != null;
|
||||||
|
|||||||
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
28
Jellyfin.Api/Attributes/AcceptsFileAttribute.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Internal produces image attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class AcceptsFileAttribute : Attribute
|
||||||
|
{
|
||||||
|
private readonly string[] _contentTypes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
|
public AcceptsFileAttribute(params string[] contentTypes)
|
||||||
|
{
|
||||||
|
_contentTypes = contentTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configured content types.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>the configured content types.</returns>
|
||||||
|
public string[] GetContentTypes() => _contentTypes;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
18
Jellyfin.Api/Attributes/AcceptsImageFileAttribute.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Produces file attribute of "image/*".
|
||||||
|
/// </summary>
|
||||||
|
public class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||||
|
{
|
||||||
|
private const string ContentType = "image/*";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public AcceptsImageFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
Normal file
12
Jellyfin.Api/Attributes/ParameterObsoleteAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Attributes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to mark a parameter as obsolete.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
|
public class ParameterObsoleteAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,7 +122,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -168,22 +168,22 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
@@ -287,7 +287,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -309,7 +309,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -333,22 +333,22 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Channels;
|
using MediaBrowser.Controller.Channels;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@@ -121,9 +120,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||||
{
|
{
|
||||||
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
var user = userId.HasValue && !userId.Equals(Guid.Empty)
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -219,14 +219,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
var streamingRequest = new HlsVideoRequestDto
|
var streamingRequest = new HlsVideoRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -250,28 +250,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
@@ -371,7 +371,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -386,14 +386,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||||
{
|
{
|
||||||
var streamingRequest = new HlsAudioRequestDto
|
var streamingRequest = new HlsAudioRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -417,28 +417,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||||
};
|
};
|
||||||
@@ -534,7 +534,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -549,14 +549,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
var streamingRequest = new VideoRequestDto
|
var streamingRequest = new VideoRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -580,28 +580,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -699,7 +699,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -714,14 +714,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var cancellationTokenSource = new CancellationTokenSource();
|
var cancellationTokenSource = new CancellationTokenSource();
|
||||||
var streamingRequest = new StreamingRequestDto
|
var streamingRequest = new StreamingRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -745,28 +745,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -869,7 +869,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -884,14 +884,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var streamingRequest = new VideoRequestDto
|
var streamingRequest = new VideoRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -915,28 +915,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1041,7 +1041,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -1056,14 +1056,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var streamingRequest = new StreamingRequestDto
|
var streamingRequest = new StreamingRequestDto
|
||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -1087,28 +1087,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
// TODO: Deprecate with new iOS app
|
// TODO: Deprecate with new iOS app
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = segmentId + Path.GetExtension(Request.Path);
|
||||||
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid segment.");
|
||||||
|
}
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
|
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file)!, false, HttpContext);
|
||||||
}
|
}
|
||||||
@@ -83,7 +89,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||||
{
|
{
|
||||||
var file = playlistId + Path.GetExtension(Request.Path);
|
var file = playlistId + Path.GetExtension(Request.Path);
|
||||||
file = Path.Combine(_serverConfigurationManager.GetTranscodePath(), file);
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.Ordinal) || Path.GetExtension(file) != ".m3u8")
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid segment.");
|
||||||
|
}
|
||||||
|
|
||||||
return GetFileResult(file, file);
|
return GetFileResult(file, file);
|
||||||
}
|
}
|
||||||
@@ -98,7 +110,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpDelete("Videos/ActiveEncodings")]
|
[HttpDelete("Videos/ActiveEncodings")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult StopEncodingProcess([FromQuery] string deviceId, [FromQuery] string playSessionId)
|
public ActionResult StopEncodingProcess(
|
||||||
|
[FromQuery, Required] string deviceId,
|
||||||
|
[FromQuery, Required] string playSessionId)
|
||||||
{
|
{
|
||||||
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@@ -130,7 +144,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
var file = segmentId + Path.GetExtension(Request.Path);
|
||||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
file = Path.Combine(transcodeFolderPath, file);
|
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid segment.");
|
||||||
|
}
|
||||||
|
|
||||||
var normalizedPlaylistId = playlistId;
|
var normalizedPlaylistId = playlistId;
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
: type;
|
: type;
|
||||||
|
|
||||||
var path = BaseItem.SupportedImageExtensions
|
var path = BaseItem.SupportedImageExtensions
|
||||||
.Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
|
.Select(i => Path.GetFullPath(Path.Combine(_applicationPaths.GeneralPath, name, filename + i)))
|
||||||
.FirstOrDefault(System.IO.File.Exists);
|
.FirstOrDefault(System.IO.File.Exists);
|
||||||
|
|
||||||
if (path == null)
|
if (path == null)
|
||||||
@@ -82,6 +82,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid image path.");
|
||||||
|
}
|
||||||
|
|
||||||
var contentType = MimeTypes.GetMimeType(path);
|
var contentType = MimeTypes.GetMimeType(path);
|
||||||
return File(System.IO.File.OpenRead(path), contentType);
|
return File(System.IO.File.OpenRead(path), contentType);
|
||||||
}
|
}
|
||||||
@@ -163,7 +168,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
|
||||||
private ActionResult GetImageFile(string basePath, string theme, string? name)
|
private ActionResult GetImageFile(string basePath, string theme, string? name)
|
||||||
{
|
{
|
||||||
var themeFolder = Path.Combine(basePath, theme);
|
var themeFolder = Path.GetFullPath(Path.Combine(basePath, theme));
|
||||||
|
|
||||||
if (Directory.Exists(themeFolder))
|
if (Directory.Exists(themeFolder))
|
||||||
{
|
{
|
||||||
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
|
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
|
||||||
@@ -171,12 +177,18 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
|
if (!path.StartsWith(basePath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid image path.");
|
||||||
|
}
|
||||||
|
|
||||||
var contentType = MimeTypes.GetMimeType(path);
|
var contentType = MimeTypes.GetMimeType(path);
|
||||||
|
|
||||||
return PhysicalFile(path, contentType);
|
return PhysicalFile(path, contentType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var allFolder = Path.Combine(basePath, "all");
|
var allFolder = Path.GetFullPath(Path.Combine(basePath, "all"));
|
||||||
if (Directory.Exists(allFolder))
|
if (Directory.Exists(allFolder))
|
||||||
{
|
{
|
||||||
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
|
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
|
||||||
@@ -184,6 +196,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
|
||||||
{
|
{
|
||||||
|
if (!path.StartsWith(basePath, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid image path.");
|
||||||
|
}
|
||||||
|
|
||||||
var contentType = MimeTypes.GetMimeType(path);
|
var contentType = MimeTypes.GetMimeType(path);
|
||||||
return PhysicalFile(path, contentType);
|
return PhysicalFile(path, contentType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}")]
|
[HttpPost("Users/{userId}/Images/{imageType}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
@@ -133,6 +134,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
[HttpPost("Users/{userId}/Images/{imageType}/{index}")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "imageType", Justification = "Imported from ServiceStack")]
|
||||||
@@ -312,6 +314,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
[HttpPost("Items/{itemId}/Images/{imageType}")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
@@ -346,6 +349,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if item not found.</returns>
|
||||||
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
[HttpPost("Items/{itemId}/Images/{imageType}/{imageIndex}")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[AcceptsImageFile]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "index", Justification = "Imported from ServiceStack")]
|
||||||
@@ -388,7 +392,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] int newIndex)
|
[FromQuery, Required] int newIndex)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
@@ -476,6 +480,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||||
@@ -505,6 +511,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
@@ -535,6 +543,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -556,6 +566,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
/// <param name="format">Optional. The <see cref="ImageFormat"/> of the returned image.</param>
|
||||||
@@ -585,6 +597,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] string? tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
@@ -614,6 +628,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -634,6 +650,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
/// <param name="tag">Optional. Supply the cache tag from the item object to receive strong caching headers.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
/// <param name="format">Determines the output format of the image - original,gif,jpg,png.</param>
|
||||||
@@ -663,6 +681,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromRoute, Required] string tag,
|
[FromRoute, Required] string tag,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromRoute, Required] ImageFormat format,
|
[FromRoute, Required] ImageFormat format,
|
||||||
@@ -693,6 +713,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -717,6 +739,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -737,7 +761,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetArtistImage(
|
public async Task<ActionResult> GetArtistImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -746,6 +770,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -772,6 +798,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -796,6 +824,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -816,7 +846,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetGenreImage(
|
public async Task<ActionResult> GetGenreImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -825,6 +855,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -851,6 +883,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -876,6 +910,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -896,7 +932,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -905,6 +941,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -930,6 +968,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -954,6 +994,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -974,7 +1016,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetMusicGenreImage(
|
public async Task<ActionResult> GetMusicGenreImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -983,6 +1025,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1009,6 +1053,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1034,6 +1080,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1054,7 +1102,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1063,6 +1111,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1088,6 +1138,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1112,6 +1164,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1132,7 +1186,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public async Task<ActionResult> GetPersonImage(
|
public async Task<ActionResult> GetPersonImage(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1141,6 +1195,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1167,6 +1223,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1192,6 +1250,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1212,7 +1272,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromRoute, Required] ImageType imageType,
|
[FromRoute, Required] ImageType imageType,
|
||||||
[FromRoute, Required] int imageIndex,
|
[FromRoute, Required] int imageIndex,
|
||||||
[FromQuery] string tag,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] ImageFormat? format,
|
[FromQuery] ImageFormat? format,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -1221,6 +1281,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1246,6 +1308,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1270,6 +1334,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1299,6 +1365,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1325,6 +1393,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1350,6 +1420,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1379,6 +1451,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1404,6 +1478,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1428,6 +1504,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1457,6 +1535,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1500,6 +1580,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1526,6 +1608,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="width">The fixed image width to return.</param>
|
/// <param name="width">The fixed image width to return.</param>
|
||||||
/// <param name="height">The fixed image height to return.</param>
|
/// <param name="height">The fixed image height to return.</param>
|
||||||
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
/// <param name="quality">Optional. Quality setting, from 0-100. Defaults to 90 and should suffice in most cases.</param>
|
||||||
|
/// <param name="fillWidth">Width of box to fill.</param>
|
||||||
|
/// <param name="fillHeight">Height of box to fill.</param>
|
||||||
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
/// <param name="cropWhitespace">Optional. Specify if whitespace should be cropped out of the image. True/False. If unspecified, whitespace will be cropped from logos and clear art.</param>
|
||||||
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
/// <param name="addPlayedIndicator">Optional. Add a played indicator.</param>
|
||||||
/// <param name="blur">Optional. Blur image.</param>
|
/// <param name="blur">Optional. Blur image.</param>
|
||||||
@@ -1555,6 +1639,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? width,
|
[FromQuery] int? width,
|
||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? quality,
|
[FromQuery] int? quality,
|
||||||
|
[FromQuery] int? fillWidth,
|
||||||
|
[FromQuery] int? fillHeight,
|
||||||
[FromQuery] bool? cropWhitespace,
|
[FromQuery] bool? cropWhitespace,
|
||||||
[FromQuery] bool? addPlayedIndicator,
|
[FromQuery] bool? addPlayedIndicator,
|
||||||
[FromQuery] int? blur,
|
[FromQuery] int? blur,
|
||||||
@@ -1597,6 +1683,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
quality,
|
quality,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
cropWhitespace,
|
cropWhitespace,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
blur,
|
blur,
|
||||||
@@ -1681,6 +1769,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
int? width,
|
int? width,
|
||||||
int? height,
|
int? height,
|
||||||
int? quality,
|
int? quality,
|
||||||
|
int? fillWidth,
|
||||||
|
int? fillHeight,
|
||||||
bool? cropWhitespace,
|
bool? cropWhitespace,
|
||||||
bool? addPlayedIndicator,
|
bool? addPlayedIndicator,
|
||||||
int? blur,
|
int? blur,
|
||||||
@@ -1744,11 +1834,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
item,
|
item,
|
||||||
itemId,
|
itemId,
|
||||||
imageIndex,
|
imageIndex,
|
||||||
height,
|
|
||||||
maxHeight,
|
|
||||||
maxWidth,
|
|
||||||
quality,
|
|
||||||
width,
|
width,
|
||||||
|
height,
|
||||||
|
maxWidth,
|
||||||
|
maxHeight,
|
||||||
|
fillWidth,
|
||||||
|
fillHeight,
|
||||||
|
quality,
|
||||||
addPlayedIndicator,
|
addPlayedIndicator,
|
||||||
percentPlayed,
|
percentPlayed,
|
||||||
unplayedCount,
|
unplayedCount,
|
||||||
@@ -1843,11 +1935,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
BaseItem? item,
|
BaseItem? item,
|
||||||
Guid itemId,
|
Guid itemId,
|
||||||
int? index,
|
int? index,
|
||||||
int? height,
|
|
||||||
int? maxHeight,
|
|
||||||
int? maxWidth,
|
|
||||||
int? quality,
|
|
||||||
int? width,
|
int? width,
|
||||||
|
int? height,
|
||||||
|
int? maxWidth,
|
||||||
|
int? maxHeight,
|
||||||
|
int? fillWidth,
|
||||||
|
int? fillHeight,
|
||||||
|
int? quality,
|
||||||
bool? addPlayedIndicator,
|
bool? addPlayedIndicator,
|
||||||
double? percentPlayed,
|
double? percentPlayed,
|
||||||
int? unplayedCount,
|
int? unplayedCount,
|
||||||
@@ -1876,6 +1970,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
ItemId = itemId,
|
ItemId = itemId,
|
||||||
MaxHeight = maxHeight,
|
MaxHeight = maxHeight,
|
||||||
MaxWidth = maxWidth,
|
MaxWidth = maxWidth,
|
||||||
|
FillHeight = fillHeight,
|
||||||
|
FillWidth = fillWidth,
|
||||||
Quality = quality ?? 100,
|
Quality = quality ?? 100,
|
||||||
Width = width,
|
Width = width,
|
||||||
AddPlayedIndicator = addPlayedIndicator ?? false,
|
AddPlayedIndicator = addPlayedIndicator ?? false,
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given album.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The item id.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -123,7 +123,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given playlist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The item id.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -159,7 +159,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given genre.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The genre name.</param>
|
/// <param name="name">The genre name.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -173,7 +173,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
[HttpGet("MusicGenres/{name}/InstantMix")]
|
[HttpGet("MusicGenres/{name}/InstantMix")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenre(
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
|
||||||
[FromRoute, Required] string name,
|
[FromRoute, Required] string name,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
@@ -194,7 +194,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given artist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The item id.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -230,7 +230,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given genre.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The item id.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -244,7 +244,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
[HttpGet("MusicGenres/{id}/InstantMix")]
|
[HttpGet("MusicGenres/{id}/InstantMix")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenres(
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
|
||||||
[FromRoute, Required] Guid id,
|
[FromRoute, Required] Guid id,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
@@ -266,7 +266,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an instant playlist based on a given song.
|
/// Creates an instant playlist based on a given item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The item id.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
@@ -301,6 +301,80 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return GetResult(items, user, limit, dtoOptions);
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given artist.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The item id.</param>
|
||||||
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
|
/// <response code="200">Instant playlist returned.</response>
|
||||||
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
|
[HttpGet("Artists/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[Obsolete("Use GetInstantMixFromArtists")]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists2(
|
||||||
|
[FromQuery, Required] Guid id,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery] bool? enableImages,
|
||||||
|
[FromQuery] bool? enableUserData,
|
||||||
|
[FromQuery] int? imageTypeLimit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||||
|
{
|
||||||
|
return GetInstantMixFromArtists(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
limit,
|
||||||
|
fields,
|
||||||
|
enableImages,
|
||||||
|
enableUserData,
|
||||||
|
imageTypeLimit,
|
||||||
|
enableImageTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given genre.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The item id.</param>
|
||||||
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
|
/// <response code="200">Instant playlist returned.</response>
|
||||||
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
|
[HttpGet("MusicGenres/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[Obsolete("Use GetInstantMixFromMusicGenres instead")]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById2(
|
||||||
|
[FromQuery, Required] Guid id,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery] bool? enableImages,
|
||||||
|
[FromQuery] bool? enableUserData,
|
||||||
|
[FromQuery] int? imageTypeLimit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||||
|
{
|
||||||
|
return GetInstantMixFromMusicGenreById(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
limit,
|
||||||
|
fields,
|
||||||
|
enableImages,
|
||||||
|
enableUserData,
|
||||||
|
imageTypeLimit,
|
||||||
|
enableImageTypes);
|
||||||
|
}
|
||||||
|
|
||||||
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var list = items;
|
var list = items;
|
||||||
|
|||||||
@@ -239,48 +239,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return Ok(results);
|
return Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a remote image.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="imageUrl">The image url.</param>
|
|
||||||
/// <param name="providerName">The provider name.</param>
|
|
||||||
/// <response code="200">Remote image retrieved.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="FileStreamResult"/> containing the images file stream.
|
|
||||||
/// </returns>
|
|
||||||
[HttpGet("Items/RemoteSearch/Image")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesImageFile]
|
|
||||||
public async Task<ActionResult> GetRemoteSearchImage(
|
|
||||||
[FromQuery, Required] string imageUrl,
|
|
||||||
[FromQuery, Required] string providerName)
|
|
||||||
{
|
|
||||||
var urlHash = imageUrl.GetMD5();
|
|
||||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
|
||||||
if (System.IO.File.Exists(contentPath))
|
|
||||||
{
|
|
||||||
return PhysicalFile(contentPath, MimeTypes.GetMimeType(contentPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
// Means the file isn't cached yet
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// Means the file isn't cached yet
|
|
||||||
}
|
|
||||||
|
|
||||||
await DownloadImage(providerName, imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
|
||||||
var updatedContentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
|
||||||
return PhysicalFile(updatedContentPath, MimeTypes.GetMimeType(updatedContentPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies search criteria to an item and refreshes metadata.
|
/// Applies search criteria to an item and refreshes metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -322,53 +280,5 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Downloads the image.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="providerName">Name of the provider.</param>
|
|
||||||
/// <param name="url">The URL.</param>
|
|
||||||
/// <param name="urlHash">The URL hash.</param>
|
|
||||||
/// <param name="pointerCachePath">The pointer cache path.</param>
|
|
||||||
/// <returns>Task.</returns>
|
|
||||||
private async Task DownloadImage(string providerName, string url, Guid urlHash, string pointerCachePath)
|
|
||||||
{
|
|
||||||
using var result = await _providerManager.GetSearchImage(providerName, url, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
if (result.Content.Headers.ContentType?.MediaType == null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException(nameof(result.Content.Headers.ContentType));
|
|
||||||
}
|
|
||||||
|
|
||||||
var ext = result.Content.Headers.ContentType.MediaType.Split('/')[^1];
|
|
||||||
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
using (var stream = result.Content)
|
|
||||||
{
|
|
||||||
await using var fileStream = new FileStream(
|
|
||||||
fullCachePath,
|
|
||||||
FileMode.Create,
|
|
||||||
FileAccess.Write,
|
|
||||||
FileShare.Read,
|
|
||||||
IODefaults.FileStreamBufferSize,
|
|
||||||
true);
|
|
||||||
|
|
||||||
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
|
||||||
|
|
||||||
Directory.CreateDirectory(pointerCacheDirectory);
|
|
||||||
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the full cache path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filename">The filename.</param>
|
|
||||||
/// <returns>System.String.</returns>
|
|
||||||
private string GetFullCachePath(string filename)
|
|
||||||
=> Path.Combine(_appPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("Items/{itemId}/ContentType")]
|
[HttpPost("Items/{itemId}/ContentType")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string contentType)
|
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
if (item == null)
|
if (item == null)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -184,7 +184,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
@@ -247,8 +247,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
folder = _libraryManager.GetUserRootFolder();
|
folder = _libraryManager.GetUserRootFolder();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folder is IHasCollectionType hasCollectionType
|
string? collectionType = null;
|
||||||
&& string.Equals(hasCollectionType.CollectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
if (folder is IHasCollectionType hasCollectionType)
|
||||||
|
{
|
||||||
|
collectionType = hasCollectionType.CollectionType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(collectionType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
recursive = true;
|
recursive = true;
|
||||||
includeItemTypes = new[] { "Playlist" };
|
includeItemTypes = new[] { "Playlist" };
|
||||||
@@ -271,10 +276,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(item is UserRootFolder)
|
if (item is not UserRootFolder
|
||||||
&& !isInEnabledFolder
|
&& !isInEnabledFolder
|
||||||
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
&& !user.HasPermission(PermissionKind.EnableAllFolders)
|
||||||
&& !user.HasPermission(PermissionKind.EnableAllChannels))
|
&& !user.HasPermission(PermissionKind.EnableAllChannels)
|
||||||
|
&& !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
_logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
|
||||||
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
|
||||||
@@ -608,7 +614,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -617,7 +623,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path));
|
return PhysicalFile(item.Path, MimeTypes.GetMimeType(item.Path), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -304,7 +304,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Library scan started.</response>
|
/// <response code="204">Library scan started.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpGet("Library/Refresh")]
|
[HttpPost("Library/Refresh")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> RefreshLibrary()
|
public async Task<ActionResult> RefreshLibrary()
|
||||||
@@ -591,15 +591,15 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reports that new movies have been added by an external source.
|
/// Reports that new movies have been added by an external source.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="updates">A list of updated media paths.</param>
|
/// <param name="dto">The update paths.</param>
|
||||||
/// <response code="204">Report success.</response>
|
/// <response code="204">Report success.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Library/Media/Updated")]
|
[HttpPost("Library/Media/Updated")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto[] updates)
|
public ActionResult PostUpdatedMedia([FromBody, Required] MediaUpdateInfoDto dto)
|
||||||
{
|
{
|
||||||
foreach (var item in updates)
|
foreach (var item in dto.Updates)
|
||||||
{
|
{
|
||||||
_libraryMonitor.ReportFileSystemChanged(item.Path);
|
_libraryMonitor.ReportFileSystemChanged(item.Path);
|
||||||
}
|
}
|
||||||
@@ -667,7 +667,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO determine non-ASCII validity.
|
// TODO determine non-ASCII validity.
|
||||||
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename);
|
return PhysicalFile(path, MimeTypes.GetMimeType(path), filename, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -778,7 +778,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
|
public ActionResult<LibraryOptionsResultDto> GetLibraryOptionsInfo(
|
||||||
[FromQuery] string? libraryContentType,
|
[FromQuery] string? libraryContentType,
|
||||||
[FromQuery] bool isNewLibrary)
|
[FromQuery] bool isNewLibrary = false)
|
||||||
{
|
{
|
||||||
var result = new LibraryOptionsResultDto();
|
var result = new LibraryOptionsResultDto();
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> AddVirtualFolder(
|
public async Task<ActionResult> AddVirtualFolder(
|
||||||
[FromQuery] string? name,
|
[FromQuery] string? name,
|
||||||
[FromQuery] string? collectionType,
|
[FromQuery] CollectionTypeOptions? collectionType,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] paths,
|
||||||
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
[FromBody] AddVirtualFolderDto? libraryOptionsDto,
|
||||||
[FromQuery] bool refreshLibrary = false)
|
[FromQuery] bool refreshLibrary = false)
|
||||||
@@ -241,23 +241,20 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates a media path.
|
/// Updates a media path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the library.</param>
|
/// <param name="mediaPathRequestDto">The name of the library and path infos.</param>
|
||||||
/// <param name="pathInfo">The path info.</param>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
/// <response code="204">Media path updated.</response>
|
/// <response code="204">Media path updated.</response>
|
||||||
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
|
||||||
[HttpPost("Paths/Update")]
|
[HttpPost("Paths/Update")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult UpdateMediaPath(
|
public ActionResult UpdateMediaPath([FromBody, Required] UpdateMediaPathRequestDto mediaPathRequestDto)
|
||||||
[FromQuery] string? name,
|
|
||||||
[FromBody] MediaPathInfo? pathInfo)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(mediaPathRequestDto.Name))
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(name));
|
throw new ArgumentNullException(nameof(mediaPathRequestDto), "Name must not be null or empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
_libraryManager.UpdateMediaPath(name, pathInfo);
|
_libraryManager.UpdateMediaPath(mediaPathRequestDto.Name, mediaPathRequestDto.PathInfo);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,8 +553,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isSports,
|
[FromQuery] bool? isSports,
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||||
[FromQuery] bool? enableImages,
|
[FromQuery] bool? enableImages,
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
|
/// Query parameters are obsolete.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="userId">The user id.</param>
|
/// <param name="userId">The user id.</param>
|
||||||
@@ -106,20 +107,20 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
public async Task<ActionResult<PlaybackInfoResponse>> GetPostedPlaybackInfo(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery, ParameterObsolete] Guid? userId,
|
||||||
[FromQuery] int? maxStreamingBitrate,
|
[FromQuery, ParameterObsolete] int? maxStreamingBitrate,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery, ParameterObsolete] long? startTimeTicks,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery, ParameterObsolete] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery, ParameterObsolete] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery, ParameterObsolete] int? maxAudioChannels,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery, ParameterObsolete] string? liveStreamId,
|
||||||
[FromQuery] bool? autoOpenLiveStream,
|
[FromQuery, ParameterObsolete] bool? autoOpenLiveStream,
|
||||||
[FromQuery] bool? enableDirectPlay,
|
[FromQuery, ParameterObsolete] bool? enableDirectPlay,
|
||||||
[FromQuery] bool? enableDirectStream,
|
[FromQuery, ParameterObsolete] bool? enableDirectStream,
|
||||||
[FromQuery] bool? enableTranscoding,
|
[FromQuery, ParameterObsolete] bool? enableTranscoding,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery, ParameterObsolete] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery, ParameterObsolete] bool? allowAudioStreamCopy,
|
||||||
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] PlaybackInfoDto? playbackInfoDto)
|
||||||
{
|
{
|
||||||
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
var authInfo = _authContext.GetAuthorizationInfo(Request);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
@@ -86,26 +87,19 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sends a notification to all admins.
|
/// Sends a notification to all admins.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url">The URL of the notification.</param>
|
/// <param name="notificationDto">The notification request.</param>
|
||||||
/// <param name="level">The level of the notification.</param>
|
|
||||||
/// <param name="name">The name of the notification.</param>
|
|
||||||
/// <param name="description">The description of the notification.</param>
|
|
||||||
/// <response code="204">Notification sent.</response>
|
/// <response code="204">Notification sent.</response>
|
||||||
/// <returns>A <cref see="NoContentResult"/>.</returns>
|
/// <returns>A <cref see="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Admin")]
|
[HttpPost("Admin")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult CreateAdminNotification(
|
public ActionResult CreateAdminNotification([FromBody, Required] AdminNotificationDto notificationDto)
|
||||||
[FromQuery] string? url,
|
|
||||||
[FromQuery] NotificationLevel? level,
|
|
||||||
[FromQuery] string name = "",
|
|
||||||
[FromQuery] string description = "")
|
|
||||||
{
|
{
|
||||||
var notification = new NotificationRequest
|
var notification = new NotificationRequest
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = notificationDto.Name,
|
||||||
Description = description,
|
Description = notificationDto.Description,
|
||||||
Url = url,
|
Url = notificationDto.Url,
|
||||||
Level = level ?? NotificationLevel.Normal,
|
Level = notificationDto.NotificationLevel ?? NotificationLevel.Normal,
|
||||||
UserIds = _userManager.Users
|
UserIds = _userManager.Users
|
||||||
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
|
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
|
||||||
.Select(user => user.Id)
|
.Select(user => user.Id)
|
||||||
@@ -114,7 +108,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
};
|
};
|
||||||
|
|
||||||
_notificationManager.SendNotification(notification, CancellationToken.None);
|
_notificationManager.SendNotification(notification, CancellationToken.None);
|
||||||
|
|
||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost("Repositories")]
|
[HttpPost("Repositories")]
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[Authorize(Policy = Policies.DefaultAuthorization)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult SetRepositories([FromBody] List<RepositoryInfo> repositoryInfos)
|
public ActionResult SetRepositories([FromBody, Required] List<RepositoryInfo> repositoryInfos)
|
||||||
{
|
{
|
||||||
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
_serverConfigurationManager.Configuration.PluginRepositories = repositoryInfos;
|
||||||
_serverConfigurationManager.SaveConfiguration();
|
_serverConfigurationManager.SaveConfiguration();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
@@ -57,6 +58,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
/// For backwards compatibility parameters can be sent via Query or Body, with Query having higher precedence.
|
||||||
|
/// Query parameters are obsolete.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="name">The playlist name.</param>
|
/// <param name="name">The playlist name.</param>
|
||||||
/// <param name="ids">The item ids.</param>
|
/// <param name="ids">The item ids.</param>
|
||||||
@@ -70,10 +72,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
public async Task<ActionResult<PlaylistCreationResult>> CreatePlaylist(
|
||||||
[FromQuery] string? name,
|
[FromQuery, ParameterObsolete] string? name,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] IReadOnlyList<Guid> ids,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder)), ParameterObsolete] IReadOnlyList<Guid> ids,
|
||||||
[FromQuery] Guid? userId,
|
[FromQuery, ParameterObsolete] Guid? userId,
|
||||||
[FromQuery] string? mediaType,
|
[FromQuery, ParameterObsolete] string? mediaType,
|
||||||
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
|
[FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] CreatePlaylistDto? createPlaylistRequest)
|
||||||
{
|
{
|
||||||
if (ids.Count == 0)
|
if (ids.Count == 0)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Sessions/Playing/Ping")]
|
[HttpPost("Sessions/Playing/Ping")]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult PingPlaybackSession([FromQuery] string playSessionId)
|
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
|
||||||
{
|
{
|
||||||
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
|
_transcodingJobHelper.PingTranscodingJob(playSessionId, null);
|
||||||
return NoContent();
|
return NoContent();
|
||||||
@@ -202,9 +202,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] PlayMethod playMethod,
|
[FromQuery] PlayMethod? playMethod,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] bool canSeek = false)
|
[FromQuery] bool canSeek = false)
|
||||||
{
|
{
|
||||||
var playbackStartInfo = new PlaybackStartInfo
|
var playbackStartInfo = new PlaybackStartInfo
|
||||||
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
MediaSourceId = mediaSourceId,
|
MediaSourceId = mediaSourceId,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
PlayMethod = playMethod,
|
PlayMethod = playMethod ?? PlayMethod.Transcode,
|
||||||
PlaySessionId = playSessionId,
|
PlaySessionId = playSessionId,
|
||||||
LiveStreamId = liveStreamId
|
LiveStreamId = liveStreamId
|
||||||
};
|
};
|
||||||
@@ -254,10 +254,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] int? volumeLevel,
|
[FromQuery] int? volumeLevel,
|
||||||
[FromQuery] PlayMethod playMethod,
|
[FromQuery] PlayMethod? playMethod,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string playSessionId,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] RepeatMode repeatMode,
|
[FromQuery] RepeatMode? repeatMode,
|
||||||
[FromQuery] bool isPaused = false,
|
[FromQuery] bool isPaused = false,
|
||||||
[FromQuery] bool isMuted = false)
|
[FromQuery] bool isMuted = false)
|
||||||
{
|
{
|
||||||
@@ -271,10 +271,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
VolumeLevel = volumeLevel,
|
VolumeLevel = volumeLevel,
|
||||||
PlayMethod = playMethod,
|
PlayMethod = playMethod ?? PlayMethod.Transcode,
|
||||||
PlaySessionId = playSessionId,
|
PlaySessionId = playSessionId,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
RepeatMode = repeatMode
|
RepeatMode = repeatMode ?? RepeatMode.RepeatNone
|
||||||
};
|
};
|
||||||
|
|
||||||
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
playbackProgressInfo.PlayMethod = ValidatePlayMethod(playbackProgressInfo.PlayMethod, playbackProgressInfo.PlaySessionId);
|
||||||
@@ -352,7 +352,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return _userDataRepository.GetUserDataDto(item, user);
|
return _userDataRepository.GetUserDataDto(item, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlayMethod ValidatePlayMethod(PlayMethod method, string playSessionId)
|
private PlayMethod ValidatePlayMethod(PlayMethod method, string? playSessionId)
|
||||||
{
|
{
|
||||||
if (method == PlayMethod.Transcode)
|
if (method == PlayMethod.Transcode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -300,9 +300,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
|
var imagePath = Path.Combine(plugin.Path, plugin.Manifest.ImagePath ?? string.Empty);
|
||||||
if (((ServerConfiguration)_config.CommonConfiguration).DisablePluginImages
|
if (plugin.Manifest.ImagePath == null || !System.IO.File.Exists(imagePath))
|
||||||
|| plugin.Manifest.ImagePath == null
|
|
||||||
|| !System.IO.File.Exists(imagePath))
|
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,58 +145,6 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a remote image.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="imageUrl">The image url.</param>
|
|
||||||
/// <response code="200">Remote image returned.</response>
|
|
||||||
/// <response code="404">Remote image not found.</response>
|
|
||||||
/// <returns>Image Stream.</returns>
|
|
||||||
[HttpGet("Images/Remote")]
|
|
||||||
[Produces(MediaTypeNames.Application.Octet)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesImageFile]
|
|
||||||
public async Task<ActionResult> GetRemoteImage([FromQuery, Required] Uri imageUrl)
|
|
||||||
{
|
|
||||||
var urlHash = imageUrl.ToString().GetMD5();
|
|
||||||
var pointerCachePath = GetFullCachePath(urlHash.ToString());
|
|
||||||
|
|
||||||
string? contentPath = null;
|
|
||||||
var hasFile = false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
|
||||||
if (System.IO.File.Exists(contentPath))
|
|
||||||
{
|
|
||||||
hasFile = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (FileNotFoundException)
|
|
||||||
{
|
|
||||||
// The file isn't cached yet
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
// The file isn't cached yet
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasFile)
|
|
||||||
{
|
|
||||||
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
|
|
||||||
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(contentPath))
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var contentType = MimeTypes.GetMimeType(contentPath);
|
|
||||||
return PhysicalFile(contentPath, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads a remote image for an item.
|
/// Downloads a remote image for an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -259,7 +207,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
var fullCacheDirectory = Path.GetDirectoryName(fullCachePath) ?? throw new ResourceNotFoundException($"Provided path ({fullCachePath}) is not valid.");
|
||||||
Directory.CreateDirectory(fullCacheDirectory);
|
Directory.CreateDirectory(fullCacheDirectory);
|
||||||
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
|
// use FileShare.None as this bypasses dotnet bug dotnet/runtime#42790 .
|
||||||
|
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, true);
|
||||||
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
await response.Content.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||||
|
|
||||||
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
var pointerCacheDirectory = Path.GetDirectoryName(pointerCachePath) ?? throw new ArgumentException($"Provided path ({pointerCachePath}) is not valid.", nameof(pointerCachePath));
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
|
/// <param name="playCommand">The type of play command to issue (PlayNow, PlayNext, PlayLast). Clients who have not yet implemented play next and play last may play now.</param>
|
||||||
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
|
/// <param name="itemIds">The ids of the items to play, comma delimited.</param>
|
||||||
/// <param name="startPositionTicks">The starting position of the first item.</param>
|
/// <param name="startPositionTicks">The starting position of the first item.</param>
|
||||||
|
/// <param name="mediaSourceId">Optional. The media source id.</param>
|
||||||
|
/// <param name="audioStreamIndex">Optional. The index of the audio stream to play.</param>
|
||||||
|
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to play.</param>
|
||||||
|
/// <param name="startIndex">Optional. The start index.</param>
|
||||||
/// <response code="204">Instruction sent to session.</response>
|
/// <response code="204">Instruction sent to session.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Sessions/{sessionId}/Playing")]
|
[HttpPost("Sessions/{sessionId}/Playing")]
|
||||||
@@ -162,13 +166,21 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromRoute, Required] string sessionId,
|
[FromRoute, Required] string sessionId,
|
||||||
[FromQuery, Required] PlayCommand playCommand,
|
[FromQuery, Required] PlayCommand playCommand,
|
||||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] itemIds,
|
||||||
[FromQuery] long? startPositionTicks)
|
[FromQuery] long? startPositionTicks,
|
||||||
|
[FromQuery] string? mediaSourceId,
|
||||||
|
[FromQuery] int? audioStreamIndex,
|
||||||
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
|
[FromQuery] int? startIndex)
|
||||||
{
|
{
|
||||||
var playRequest = new PlayRequest
|
var playRequest = new PlayRequest
|
||||||
{
|
{
|
||||||
ItemIds = itemIds,
|
ItemIds = itemIds,
|
||||||
StartPositionTicks = startPositionTicks,
|
StartPositionTicks = startPositionTicks,
|
||||||
PlayCommand = playCommand
|
PlayCommand = playCommand,
|
||||||
|
MediaSourceId = mediaSourceId,
|
||||||
|
AudioStreamIndex = audioStreamIndex,
|
||||||
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
|
StartIndex = startIndex
|
||||||
};
|
};
|
||||||
|
|
||||||
_sessionManager.SendPlayCommand(
|
_sessionManager.SendPlayCommand(
|
||||||
@@ -301,9 +313,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// Issues a command to a client to display a message to the user.
|
/// Issues a command to a client to display a message to the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sessionId">The session id.</param>
|
/// <param name="sessionId">The session id.</param>
|
||||||
/// <param name="text">The message test.</param>
|
/// <param name="command">The <see cref="MessageCommand" /> object containing Header, Message Text, and TimeoutMs.</param>
|
||||||
/// <param name="header">The message header.</param>
|
|
||||||
/// <param name="timeoutMs">The message timeout. If omitted the user will have to confirm viewing the message.</param>
|
|
||||||
/// <response code="204">Message sent.</response>
|
/// <response code="204">Message sent.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Sessions/{sessionId}/Message")]
|
[HttpPost("Sessions/{sessionId}/Message")]
|
||||||
@@ -311,16 +321,12 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult SendMessageCommand(
|
public ActionResult SendMessageCommand(
|
||||||
[FromRoute, Required] string sessionId,
|
[FromRoute, Required] string sessionId,
|
||||||
[FromQuery, Required] string text,
|
[FromBody, Required] MessageCommand command)
|
||||||
[FromQuery] string? header,
|
|
||||||
[FromQuery] long? timeoutMs)
|
|
||||||
{
|
{
|
||||||
var command = new MessageCommand
|
if (string.IsNullOrWhiteSpace(command.Header))
|
||||||
{
|
{
|
||||||
Header = string.IsNullOrEmpty(header) ? "Message from Server" : header,
|
command.Header = "Message from Server";
|
||||||
TimeoutMs = timeoutMs,
|
}
|
||||||
Text = text
|
|
||||||
};
|
|
||||||
|
|
||||||
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
|
_sessionManager.SendMessageCommand(RequestHelpers.GetSession(_sessionManager, _authContext, Request).Id, sessionId, command, CancellationToken.None);
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,10 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets subtitles in a specified format.
|
/// Gets subtitles in a specified format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="routeItemId">The (route) item id.</param>
|
||||||
|
/// <param name="routeMediaSourceId">The (route) media source id.</param>
|
||||||
|
/// <param name="routeIndex">The (route) subtitle stream index.</param>
|
||||||
|
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="mediaSourceId">The media source id.</param>
|
/// <param name="mediaSourceId">The media source id.</param>
|
||||||
/// <param name="index">The subtitle stream index.</param>
|
/// <param name="index">The subtitle stream index.</param>
|
||||||
@@ -189,22 +193,32 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
|
||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
|
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/Stream.{routeFormat}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesFile("text/*")]
|
[ProducesFile("text/*")]
|
||||||
public async Task<ActionResult> GetSubtitle(
|
public async Task<ActionResult> GetSubtitle(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid routeItemId,
|
||||||
[FromRoute, Required] string mediaSourceId,
|
[FromRoute, Required] string routeMediaSourceId,
|
||||||
[FromRoute, Required] int index,
|
[FromRoute, Required] int routeIndex,
|
||||||
[FromRoute, Required] string format,
|
[FromRoute, Required] string routeFormat,
|
||||||
|
[FromQuery, ParameterObsolete] Guid? itemId,
|
||||||
|
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||||
|
[FromQuery, ParameterObsolete] int? index,
|
||||||
|
[FromQuery, ParameterObsolete] string? format,
|
||||||
[FromQuery] long? endPositionTicks,
|
[FromQuery] long? endPositionTicks,
|
||||||
[FromQuery] bool copyTimestamps = false,
|
[FromQuery] bool copyTimestamps = false,
|
||||||
[FromQuery] bool addVttTimeMap = false,
|
[FromQuery] bool addVttTimeMap = false,
|
||||||
[FromQuery] long startPositionTicks = 0)
|
[FromQuery] long startPositionTicks = 0)
|
||||||
{
|
{
|
||||||
|
// Set parameters to route value if not provided via query.
|
||||||
|
itemId ??= routeItemId;
|
||||||
|
mediaSourceId ??= routeMediaSourceId;
|
||||||
|
index ??= routeIndex;
|
||||||
|
format ??= routeFormat;
|
||||||
|
|
||||||
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
format = "json";
|
format = "json";
|
||||||
@@ -212,9 +226,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (string.IsNullOrEmpty(format))
|
if (string.IsNullOrEmpty(format))
|
||||||
{
|
{
|
||||||
var item = (Video)_libraryManager.GetItemById(itemId);
|
var item = (Video)_libraryManager.GetItemById(itemId.Value);
|
||||||
|
|
||||||
var idString = itemId.ToString("N", CultureInfo.InvariantCulture);
|
var idString = itemId.Value.ToString("N", CultureInfo.InvariantCulture);
|
||||||
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
|
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
|
||||||
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
|
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
|
||||||
|
|
||||||
@@ -226,7 +240,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
|
||||||
{
|
{
|
||||||
await using Stream stream = await EncodeSubtitles(itemId, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
|
await using Stream stream = await EncodeSubtitles(itemId.Value, mediaSourceId, index.Value, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
|
|
||||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
@@ -238,9 +252,9 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
return File(
|
return File(
|
||||||
await EncodeSubtitles(
|
await EncodeSubtitles(
|
||||||
itemId,
|
itemId.Value,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
index,
|
index.Value,
|
||||||
format,
|
format,
|
||||||
startPositionTicks,
|
startPositionTicks,
|
||||||
endPositionTicks,
|
endPositionTicks,
|
||||||
@@ -251,30 +265,44 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets subtitles in a specified format.
|
/// Gets subtitles in a specified format.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="routeItemId">The (route) item id.</param>
|
||||||
|
/// <param name="routeMediaSourceId">The (route) media source id.</param>
|
||||||
|
/// <param name="routeIndex">The (route) subtitle stream index.</param>
|
||||||
|
/// <param name="routeStartPositionTicks">The (route) start position of the subtitle in ticks.</param>
|
||||||
|
/// <param name="routeFormat">The (route) format of the returned subtitle.</param>
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="mediaSourceId">The media source id.</param>
|
/// <param name="mediaSourceId">The media source id.</param>
|
||||||
/// <param name="index">The subtitle stream index.</param>
|
/// <param name="index">The subtitle stream index.</param>
|
||||||
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
|
/// <param name="startPositionTicks">The start position of the subtitle in ticks.</param>
|
||||||
/// <param name="format">The format of the returned subtitle.</param>
|
/// <param name="format">The format of the returned subtitle.</param>
|
||||||
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
|
||||||
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
|
||||||
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
|
||||||
/// <response code="200">File returned.</response>
|
/// <response code="200">File returned.</response>
|
||||||
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
|
||||||
[HttpGet("Videos/{itemId}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
|
[HttpGet("Videos/{routeItemId}/{routeMediaSourceId}/Subtitles/{routeIndex}/{routeStartPositionTicks}/Stream.{routeFormat}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesFile("text/*")]
|
[ProducesFile("text/*")]
|
||||||
public Task<ActionResult> GetSubtitleWithTicks(
|
public Task<ActionResult> GetSubtitleWithTicks(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid routeItemId,
|
||||||
[FromRoute, Required] string mediaSourceId,
|
[FromRoute, Required] string routeMediaSourceId,
|
||||||
[FromRoute, Required] int index,
|
[FromRoute, Required] int routeIndex,
|
||||||
[FromRoute, Required] long startPositionTicks,
|
[FromRoute, Required] long routeStartPositionTicks,
|
||||||
[FromRoute, Required] string format,
|
[FromRoute, Required] string routeFormat,
|
||||||
|
[FromQuery, ParameterObsolete] Guid? itemId,
|
||||||
|
[FromQuery, ParameterObsolete] string? mediaSourceId,
|
||||||
|
[FromQuery, ParameterObsolete] int? index,
|
||||||
|
[FromQuery, ParameterObsolete] long? startPositionTicks,
|
||||||
|
[FromQuery, ParameterObsolete] string? format,
|
||||||
[FromQuery] long? endPositionTicks,
|
[FromQuery] long? endPositionTicks,
|
||||||
[FromQuery] bool copyTimestamps = false,
|
[FromQuery] bool copyTimestamps = false,
|
||||||
[FromQuery] bool addVttTimeMap = false)
|
[FromQuery] bool addVttTimeMap = false)
|
||||||
{
|
{
|
||||||
return GetSubtitle(
|
return GetSubtitle(
|
||||||
|
routeItemId,
|
||||||
|
routeMediaSourceId,
|
||||||
|
routeIndex,
|
||||||
|
routeFormat,
|
||||||
itemId,
|
itemId,
|
||||||
mediaSourceId,
|
mediaSourceId,
|
||||||
index,
|
index,
|
||||||
@@ -282,7 +310,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
endPositionTicks,
|
endPositionTicks,
|
||||||
copyTimestamps,
|
copyTimestamps,
|
||||||
addVttTimeMap,
|
addVttTimeMap,
|
||||||
startPositionTicks);
|
startPositionTicks ?? routeStartPositionTicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -371,6 +399,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <response code="204">Subtitle uploaded.</response>
|
/// <response code="204">Subtitle uploaded.</response>
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
[HttpPost("Videos/{itemId}/Subtitles")]
|
[HttpPost("Videos/{itemId}/Subtitles")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public async Task<ActionResult> UploadSubtitle(
|
public async Task<ActionResult> UploadSubtitle(
|
||||||
[FromRoute, Required] Guid itemId,
|
[FromRoute, Required] Guid itemId,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
using MediaBrowser.Model.Entities;
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
@@ -144,7 +145,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] bool? recursive,
|
[FromQuery] bool? recursive,
|
||||||
[FromQuery] string? searchTerm,
|
[FromQuery] string? searchTerm,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
@@ -152,7 +153,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] bool? isFavorite,
|
[FromQuery] bool? isFavorite,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] imageTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? isPlayed,
|
[FromQuery] bool? isPlayed,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
/// <param name="enableUserData">Optional. Include user data.</param>
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||||
|
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||||
[HttpGet("NextUp")]
|
[HttpGet("NextUp")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
@@ -81,7 +82,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? imageTypeLimit,
|
[FromQuery] int? imageTypeLimit,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
[FromQuery] bool? enableUserData,
|
[FromQuery] bool? enableUserData,
|
||||||
[FromQuery] bool enableTotalRecordCount = true)
|
[FromQuery] bool enableTotalRecordCount = true,
|
||||||
|
[FromQuery] bool disableFirstEpisode = false)
|
||||||
{
|
{
|
||||||
var options = new DtoOptions { Fields = fields }
|
var options = new DtoOptions { Fields = fields }
|
||||||
.AddClientFields(Request)
|
.AddClientFields(Request)
|
||||||
@@ -95,7 +97,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
SeriesId = seriesId,
|
SeriesId = seriesId,
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
UserId = userId ?? Guid.Empty,
|
UserId = userId ?? Guid.Empty,
|
||||||
EnableTotalRecordCount = enableTotalRecordCount
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
DisableFirstEpisode = disableFirstEpisode
|
||||||
},
|
},
|
||||||
options);
|
options);
|
||||||
|
|
||||||
@@ -267,7 +270,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
if (startItemId.HasValue)
|
if (startItemId.HasValue)
|
||||||
{
|
{
|
||||||
episodes = episodes
|
episodes = episodes
|
||||||
.SkipWhile(i => startItemId.Value.Equals(i.Id))
|
.SkipWhile(i => !startItemId.Value.Equals(i.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? maxAudioSampleRate,
|
[FromQuery] int? maxAudioSampleRate,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] bool? enableRemoteMedia,
|
[FromQuery] bool? enableRemoteMedia,
|
||||||
[FromQuery] bool breakOnNonKeyFrames,
|
[FromQuery] bool breakOnNonKeyFrames = false,
|
||||||
[FromQuery] bool enableRedirection = true)
|
[FromQuery] bool enableRedirection = true)
|
||||||
{
|
{
|
||||||
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
var deviceProfile = GetDeviceProfile(container, transcodingContainer, audioCodec, transcodingProtocol, breakOnNonKeyFrames, transcodingAudioChannels, maxAudioSampleRate, maxAudioBitDepth, maxAudioChannels);
|
||||||
@@ -219,11 +219,11 @@ namespace Jellyfin.Api.Controllers
|
|||||||
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
AudioBitRate = audioBitRate ?? maxStreamingBitrate,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
SubtitleMethod = SubtitleDeliveryMethod.Hls,
|
||||||
RequireAvc = true,
|
RequireAvc = false,
|
||||||
DeInterlace = true,
|
DeInterlace = false,
|
||||||
RequireNonAnamorphic = true,
|
RequireNonAnamorphic = false,
|
||||||
EnableMpegtsM2TsMode = true,
|
EnableMpegtsM2TsMode = false,
|
||||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(',', mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||||
Context = EncodingContext.Static,
|
Context = EncodingContext.Static,
|
||||||
StreamOptions = new Dictionary<string, string>(),
|
StreamOptions = new Dictionary<string, string>(),
|
||||||
EnableAdaptiveBitrateStreaming = true
|
EnableAdaptiveBitrateStreaming = true
|
||||||
@@ -251,7 +251,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
|
AudioBitRate = isStatic ? (int?)null : (audioBitRate ?? maxStreamingBitrate),
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
AudioChannels = maxAudioChannels,
|
AudioChannels = maxAudioChannels,
|
||||||
CopyTimestamps = true,
|
CopyTimestamps = false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
SubtitleMethod = SubtitleDeliveryMethod.Embed,
|
SubtitleMethod = SubtitleDeliveryMethod.Embed,
|
||||||
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
TranscodeReasons = mediaSource.TranscodeReasons == null ? null : string.Join(",", mediaSource.TranscodeReasons.Select(i => i.ToString()).ToArray()),
|
||||||
|
|||||||
@@ -509,14 +509,14 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Redeems a forgot password pin.
|
/// Redeems a forgot password pin.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin.</param>
|
/// <param name="forgotPasswordPinRequest">The forgot password pin request containing the entered pin.</param>
|
||||||
/// <response code="200">Pin reset process started.</response>
|
/// <response code="200">Pin reset process started.</response>
|
||||||
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
|
/// <returns>A <see cref="Task"/> containing a <see cref="PinRedeemResult"/>.</returns>
|
||||||
[HttpPost("ForgotPassword/Pin")]
|
[HttpPost("ForgotPassword/Pin")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody] string? pin)
|
public async Task<ActionResult<PinRedeemResult>> ForgotPasswordPin([FromBody, Required] ForgotPasswordPinDto forgotPasswordPinRequest)
|
||||||
{
|
{
|
||||||
var result = await _userManager.RedeemPasswordResetPin(pin).ConfigureAwait(false);
|
var result = await _userManager.RedeemPasswordResetPin(forgotPasswordPinRequest.Pin).ConfigureAwait(false);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -214,7 +214,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions,
|
[FromQuery] Dictionary<string, string> streamOptions,
|
||||||
[FromQuery] int? maxWidth,
|
[FromQuery] int? maxWidth,
|
||||||
[FromQuery] int? maxHeight,
|
[FromQuery] int? maxHeight,
|
||||||
@@ -224,7 +224,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -248,28 +248,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions,
|
StreamOptions = streamOptions,
|
||||||
MaxHeight = maxHeight,
|
MaxHeight = maxHeight,
|
||||||
MaxWidth = maxWidth,
|
MaxWidth = maxWidth,
|
||||||
|
|||||||
@@ -217,9 +217,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
return BadRequest("Please supply at least two videos to merge.");
|
return BadRequest("Please supply at least two videos to merge.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var videosWithVersions = items.Where(i => i.MediaSourceCount > 1).ToList();
|
var primaryVersion = items.FirstOrDefault(i => i.MediaSourceCount > 1 && string.IsNullOrEmpty(i.PrimaryVersionId));
|
||||||
|
|
||||||
var primaryVersion = videosWithVersions.FirstOrDefault();
|
|
||||||
if (primaryVersion == null)
|
if (primaryVersion == null)
|
||||||
{
|
{
|
||||||
primaryVersion = items
|
primaryVersion = items
|
||||||
@@ -364,7 +362,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -379,7 +377,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
var isHeadRequest = Request.Method == System.Net.WebRequestMethods.Http.Head;
|
||||||
@@ -388,7 +386,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
{
|
{
|
||||||
Id = itemId,
|
Id = itemId,
|
||||||
Container = container,
|
Container = container,
|
||||||
Static = @static ?? true,
|
Static = @static ?? false,
|
||||||
Params = @params,
|
Params = @params,
|
||||||
Tag = tag,
|
Tag = tag,
|
||||||
DeviceProfileId = deviceProfileId,
|
DeviceProfileId = deviceProfileId,
|
||||||
@@ -412,28 +410,28 @@ namespace Jellyfin.Api.Controllers
|
|||||||
Level = level,
|
Level = level,
|
||||||
Framerate = framerate,
|
Framerate = framerate,
|
||||||
MaxFramerate = maxFramerate,
|
MaxFramerate = maxFramerate,
|
||||||
CopyTimestamps = copyTimestamps ?? true,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
StartTimeTicks = startTimeTicks,
|
StartTimeTicks = startTimeTicks,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height,
|
Height = height,
|
||||||
VideoBitRate = videoBitRate,
|
VideoBitRate = videoBitRate,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
SubtitleMethod = subtitleMethod,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
MaxRefFrames = maxRefFrames,
|
MaxRefFrames = maxRefFrames,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
RequireAvc = requireAvc ?? true,
|
RequireAvc = requireAvc ?? false,
|
||||||
DeInterlace = deInterlace ?? true,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? true,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
LiveStreamId = liveStreamId,
|
LiveStreamId = liveStreamId,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? true,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
VideoCodec = videoCodec,
|
VideoCodec = videoCodec,
|
||||||
SubtitleCodec = subtitleCodec,
|
SubtitleCodec = subtitleCodec,
|
||||||
TranscodeReasons = transcodeReasons,
|
TranscodeReasons = transcodeReasons,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
Context = context,
|
Context = context ?? EncodingContext.Streaming,
|
||||||
StreamOptions = streamOptions
|
StreamOptions = streamOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -540,7 +538,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||||
/// <param name="playSessionId">The play session id.</param>
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
/// <param name="segmentContainer">The segment container.</param>
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
/// <param name="segmentLength">The segment lenght.</param>
|
/// <param name="segmentLength">The segment length.</param>
|
||||||
/// <param name="minSegments">The minimum number of segments.</param>
|
/// <param name="minSegments">The minimum number of segments.</param>
|
||||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
@@ -569,7 +567,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
||||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||||
/// <param name="liveStreamId">The live stream id.</param>
|
/// <param name="liveStreamId">The live stream id.</param>
|
||||||
@@ -583,8 +581,8 @@ namespace Jellyfin.Api.Controllers
|
|||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
/// <response code="200">Video stream returned.</response>
|
/// <response code="200">Video stream returned.</response>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[HttpGet("{itemId}/{stream=stream}.{container}")]
|
[HttpGet("{itemId}/stream.{container}")]
|
||||||
[HttpHead("{itemId}/{stream=stream}.{container}", Name = "HeadVideoStreamByContainer")]
|
[HttpHead("{itemId}/stream.{container}", Name = "HeadVideoStreamByContainer")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesVideoFile]
|
[ProducesVideoFile]
|
||||||
public Task<ActionResult> GetVideoStreamByContainer(
|
public Task<ActionResult> GetVideoStreamByContainer(
|
||||||
@@ -620,7 +618,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] int? height,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] SubtitleDeliveryMethod subtitleMethod,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] bool? requireAvc,
|
||||||
@@ -635,7 +633,7 @@ namespace Jellyfin.Api.Controllers
|
|||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] int? audioStreamIndex,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] int? videoStreamIndex,
|
||||||
[FromQuery] EncodingContext context,
|
[FromQuery] EncodingContext? context,
|
||||||
[FromQuery] Dictionary<string, string> streamOptions)
|
[FromQuery] Dictionary<string, string> streamOptions)
|
||||||
{
|
{
|
||||||
return GetVideoStream(
|
return GetVideoStream(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Jellyfin.Api.Extensions;
|
|||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
@@ -70,13 +71,13 @@ namespace Jellyfin.Api.Controllers
|
|||||||
public ActionResult<QueryResult<BaseItemDto>> GetYears(
|
public ActionResult<QueryResult<BaseItemDto>> GetYears(
|
||||||
[FromQuery] int? startIndex,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery] int? limit,
|
[FromQuery] int? limit,
|
||||||
[FromQuery] string? sortOrder,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
[FromQuery] Guid? parentId,
|
[FromQuery] Guid? parentId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] excludeItemTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] includeItemTypes,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
[FromQuery] string? sortBy,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
[FromQuery] bool? enableUserData,
|
[FromQuery] bool? enableUserData,
|
||||||
[FromQuery] int? imageTypeLimit,
|
[FromQuery] int? imageTypeLimit,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
if (framerate.HasValue)
|
if (framerate.HasValue)
|
||||||
{
|
{
|
||||||
builder.Append(",FRAME-RATE=")
|
builder.Append(",FRAME-RATE=")
|
||||||
.Append(framerate.Value);
|
.Append(framerate.Value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return new FileContentResult(Array.Empty<byte>(), contentType);
|
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
|
||||||
|
return new OkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
|
return new FileStreamResult(await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), contentType);
|
||||||
@@ -68,10 +69,10 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
httpContext.Response.ContentType = contentType;
|
httpContext.Response.ContentType = contentType;
|
||||||
|
|
||||||
// if the request is a head request, return a NoContent result with the same headers as it would with a GET request
|
// if the request is a head request, return an OkResult (200) with the same headers as it would with a GET request
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return new NoContentResult();
|
return new OkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
|
return new PhysicalFileResult(path, contentType) { EnableRangeProcessing = true };
|
||||||
@@ -107,7 +108,8 @@ namespace Jellyfin.Api.Helpers
|
|||||||
// Headers only
|
// Headers only
|
||||||
if (isHeadRequest)
|
if (isHeadRequest)
|
||||||
{
|
{
|
||||||
return new FileContentResult(Array.Empty<byte>(), contentType);
|
httpContext.Response.Headers[HeaderNames.ContentType] = contentType;
|
||||||
|
return new OkResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath);
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
if (streamInfo != null)
|
if (streamInfo != null)
|
||||||
{
|
{
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,7 +308,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
{
|
{
|
||||||
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
|
||||||
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
|
||||||
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
&& user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
|
||||||
{
|
{
|
||||||
options.ForceDirectStream = true;
|
options.ForceDirectStream = true;
|
||||||
}
|
}
|
||||||
@@ -326,6 +327,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
if (streamInfo != null)
|
if (streamInfo != null)
|
||||||
{
|
{
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,6 +355,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
// Do this after the above so that StartPositionTicks is set
|
// Do this after the above so that StartPositionTicks is set
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -390,6 +393,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
// Do this after the above so that StartPositionTicks is set
|
// Do this after the above so that StartPositionTicks is set
|
||||||
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
SetDeviceSpecificSubtitleInfo(streamInfo, mediaSource, auth.Token);
|
||||||
|
mediaSource.DefaultAudioStreamIndex = streamInfo.AudioStreamIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@@ -8,7 +9,6 @@ using MediaBrowser.Controller.Entities;
|
|||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
using MediaBrowser.Model.Entities;
|
|
||||||
using MediaBrowser.Model.Querying;
|
using MediaBrowser.Model.Querying;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@@ -25,35 +25,27 @@ namespace Jellyfin.Api.Helpers
|
|||||||
/// <param name="sortBy">Sort By. Comma delimited string.</param>
|
/// <param name="sortBy">Sort By. Comma delimited string.</param>
|
||||||
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
|
/// <param name="requestedSortOrder">Sort Order. Comma delimited string.</param>
|
||||||
/// <returns>Order By.</returns>
|
/// <returns>Order By.</returns>
|
||||||
public static ValueTuple<string, SortOrder>[] GetOrderBy(string? sortBy, string? requestedSortOrder)
|
public static (string, SortOrder)[] GetOrderBy(IReadOnlyList<string> sortBy, IReadOnlyList<SortOrder> requestedSortOrder)
|
||||||
{
|
{
|
||||||
var val = sortBy;
|
if (sortBy.Count == 0)
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(val))
|
|
||||||
{
|
{
|
||||||
return Array.Empty<ValueTuple<string, SortOrder>>();
|
return Array.Empty<ValueTuple<string, SortOrder>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
var vals = val.Split(',');
|
var result = new (string, SortOrder)[sortBy.Count];
|
||||||
if (string.IsNullOrWhiteSpace(requestedSortOrder))
|
var i = 0;
|
||||||
|
// Add elements which have a SortOrder specified
|
||||||
|
for (; i < requestedSortOrder.Count; i++)
|
||||||
{
|
{
|
||||||
requestedSortOrder = "Ascending";
|
result[i] = (sortBy[i], requestedSortOrder[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sortOrders = requestedSortOrder.Split(',');
|
// Add remaining elements with the first specified SortOrder
|
||||||
|
// or the default one if no SortOrders are specified
|
||||||
var result = new ValueTuple<string, SortOrder>[vals.Length];
|
var order = requestedSortOrder.Count > 0 ? requestedSortOrder[0] : SortOrder.Ascending;
|
||||||
|
for (; i < sortBy.Count; i++)
|
||||||
for (var i = 0; i < vals.Length; i++)
|
|
||||||
{
|
{
|
||||||
var sortOrderIndex = sortOrders.Length > i ? i : 0;
|
result[i] = (sortBy[i], order);
|
||||||
|
|
||||||
var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
|
|
||||||
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? SortOrder.Descending
|
|
||||||
: SortOrder.Ascending;
|
|
||||||
|
|
||||||
result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ namespace Jellyfin.Api.Helpers
|
|||||||
&& !state.VideoRequest.MaxHeight.HasValue;
|
&& !state.VideoRequest.MaxHeight.HasValue;
|
||||||
|
|
||||||
if (isVideoResolutionNotRequested
|
if (isVideoResolutionNotRequested
|
||||||
|
&& state.VideoStream != null
|
||||||
&& state.VideoRequest.VideoBitRate.HasValue
|
&& state.VideoRequest.VideoBitRate.HasValue
|
||||||
&& state.VideoStream.BitRate.HasValue
|
&& state.VideoStream.BitRate.HasValue
|
||||||
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
|
&& state.VideoRequest.VideoBitRate.Value >= state.VideoStream.BitRate.Value)
|
||||||
@@ -507,17 +508,15 @@ namespace Jellyfin.Api.Helpers
|
|||||||
|
|
||||||
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
|
private static void ApplyDeviceProfileSettings(StreamState state, IDlnaManager dlnaManager, IDeviceManager deviceManager, HttpRequest request, string? deviceProfileId, bool? @static)
|
||||||
{
|
{
|
||||||
var headers = request.Headers;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
||||||
{
|
{
|
||||||
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
|
state.DeviceProfile = dlnaManager.GetProfile(deviceProfileId);
|
||||||
}
|
|
||||||
else if (!string.IsNullOrWhiteSpace(deviceProfileId))
|
|
||||||
{
|
|
||||||
var caps = deviceManager.GetCapabilities(deviceProfileId);
|
|
||||||
|
|
||||||
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(headers) : caps.DeviceProfile;
|
if (state.DeviceProfile == null)
|
||||||
|
{
|
||||||
|
var caps = deviceManager.GetCapabilities(deviceProfileId);
|
||||||
|
state.DeviceProfile = caps == null ? dlnaManager.GetProfile(request.Headers) : caps.DeviceProfile;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var profile = state.DeviceProfile;
|
var profile = state.DeviceProfile;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="5.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.6.3" />
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
|
|||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "Error converting value.");
|
_logger.LogDebug(e, "Error converting value.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ namespace Jellyfin.Api.ModelBinders
|
|||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "Error converting value.");
|
_logger.LogDebug(e, "Error converting value.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ namespace Jellyfin.Api.ModelBinders
|
|||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException e)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(e, "Error converting value.");
|
_logger.LogDebug(e, "Error converting value.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
namespace Jellyfin.Api.Models.LibraryDtos
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Models.LibraryDtos
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Media Update Info Dto.
|
/// Media Update Info Dto.
|
||||||
@@ -6,14 +9,8 @@
|
|||||||
public class MediaUpdateInfoDto
|
public class MediaUpdateInfoDto
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets media path.
|
/// Gets or sets the list of updates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Path { get; set; }
|
public IReadOnlyList<MediaUpdateInfoPathDto> Updates { get; set; } = Array.Empty<MediaUpdateInfoPathDto>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets media update type.
|
|
||||||
/// Created, Modified, Deleted.
|
|
||||||
/// </summary>
|
|
||||||
public string? UpdateType { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
Normal file
19
Jellyfin.Api/Models/LibraryDtos/MediaUpdateInfoPathDto.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
namespace Jellyfin.Api.Models.LibraryDtos
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The media update info path.
|
||||||
|
/// </summary>
|
||||||
|
public class MediaUpdateInfoPathDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets media path.
|
||||||
|
/// </summary>
|
||||||
|
public string? Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets media update type.
|
||||||
|
/// Created, Modified, Deleted.
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using MediaBrowser.Model.Configuration;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Models.LibraryStructureDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Update library options dto.
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateMediaPathRequestDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the library name.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets library folder path information.
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public MediaPathInfo PathInfo { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user