mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-31 04:05:50 -04:00
Merge branch 'master' into network-rewrite
This commit is contained in:
commit
3a91c37283
10
.github/workflows/automation.yml
vendored
10
.github/workflows/automation.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
if: ${{ github.repository == 'jellyfin/jellyfin' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Remove from 'Current Release' project
|
- name: Remove from 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
|
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@ -36,7 +36,7 @@ jobs:
|
|||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Release Next' project
|
- name: Add to 'Release Next' project
|
||||||
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
|
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@ -45,7 +45,7 @@ jobs:
|
|||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add to 'Current Release' project
|
- name: Add to 'Current Release' project
|
||||||
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
|
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||||
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@ -59,7 +59,7 @@ jobs:
|
|||||||
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
|
||||||
|
|
||||||
- name: Move issue to needs triage
|
- name: Move issue to needs triage
|
||||||
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
|
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||||
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@ -68,7 +68,7 @@ jobs:
|
|||||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
|
||||||
- name: Add issue to triage project
|
- name: Add issue to triage project
|
||||||
uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
|
uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3
|
||||||
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
if: github.event.issue.pull_request == '' && github.event.action == 'opened'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -27,11 +27,11 @@ jobs:
|
|||||||
dotnet-version: '7.0.x'
|
dotnet-version: '7.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
|
uses: github/codeql-action/init@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
|
uses: github/codeql-action/autobuild@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@a34ca99b4610d924e04c68db79e503e1f79f9f02 # v2
|
uses: github/codeql-action/analyze@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2
|
||||||
|
10
.github/workflows/commands.yml
vendored
10
.github/workflows/commands.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
comment-id: ${{ github.event.comment.id }}
|
comment-id: ${{ github.event.comment.id }}
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify as seen
|
- name: Notify as seen
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ github.event.comment != null }}
|
if: ${{ github.event.comment != null }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Notify as running
|
- name: Notify as running
|
||||||
id: comment_running
|
id: comment_running
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ github.event.comment != null }}
|
if: ${{ github.event.comment != null }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -93,7 +93,7 @@ jobs:
|
|||||||
exit ${retcode}
|
exit ${retcode}
|
||||||
|
|
||||||
- name: Notify with result success
|
- name: Notify with result success
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ github.event.comment != null && success() }}
|
if: ${{ github.event.comment != null && success() }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
@ -108,7 +108,7 @@ jobs:
|
|||||||
reactions: hooray
|
reactions: hooray
|
||||||
|
|
||||||
- name: Notify with result failure
|
- name: Notify with result failure
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ github.event.comment != null && failure() }}
|
if: ${{ github.event.comment != null && failure() }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
|
6
.github/workflows/openapi.yml
vendored
6
.github/workflows/openapi.yml
vendored
@ -103,14 +103,14 @@ jobs:
|
|||||||
body="${body//$'\r'/'%0D'}"
|
body="${body//$'\r'/'%0D'}"
|
||||||
echo ::set-output name=body::$body
|
echo ::set-output name=body::$body
|
||||||
- name: Find difference comment
|
- name: Find difference comment
|
||||||
uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
|
uses: peter-evans/find-comment@85a676a52594b4481e0532825a2d8906ef96dac2 # v2
|
||||||
id: find-comment
|
id: find-comment
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
direction: last
|
direction: last
|
||||||
body-includes: openapi-diff-workflow-comment
|
body-includes: openapi-diff-workflow-comment
|
||||||
- name: Reply or edit difference comment (changed)
|
- name: Reply or edit difference comment (changed)
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
@ -125,7 +125,7 @@ jobs:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
- name: Edit difference comment (unchanged)
|
- name: Edit difference comment (unchanged)
|
||||||
uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
|
uses: peter-evans/create-or-update-comment@67dcc547d311b736a8e6c5c236542148a47adc3d # v2
|
||||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
|
3
.npmrc
3
.npmrc
@ -1,3 +0,0 @@
|
|||||||
registry=https://registry.npmjs.org/
|
|
||||||
@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
|
|
||||||
always-auth=true
|
|
@ -58,6 +58,7 @@
|
|||||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||||
- [ikomhoog](https://github.com/ikomhoog)
|
- [ikomhoog](https://github.com/ikomhoog)
|
||||||
- [jftuga](https://github.com/jftuga)
|
- [jftuga](https://github.com/jftuga)
|
||||||
|
- [jmshrv](https://github.com/jmshrv)
|
||||||
- [joern-h](https://github.com/joern-h)
|
- [joern-h](https://github.com/joern-h)
|
||||||
- [joshuaboniface](https://github.com/joshuaboniface)
|
- [joshuaboniface](https://github.com/joshuaboniface)
|
||||||
- [JustAMan](https://github.com/JustAMan)
|
- [JustAMan](https://github.com/JustAMan)
|
||||||
@ -231,3 +232,4 @@
|
|||||||
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
- [Matthew Jones](https://github.com/matthew-jones-uk)
|
||||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||||
|
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||||
|
90
Directory.Packages.props
Normal file
90
Directory.Packages.props
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||||
|
|
||||||
|
<ItemGroup Label="Package Dependencies">
|
||||||
|
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.17.0" />
|
||||||
|
<PackageVersion Include="AutoFixture.Xunit2" Version="4.17.0" />
|
||||||
|
<PackageVersion Include="AutoFixture" Version="4.17.0" />
|
||||||
|
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||||
|
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
|
||||||
|
<PackageVersion Include="BlurHashSharp" Version="1.2.0" />
|
||||||
|
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||||
|
<PackageVersion Include="coverlet.collector" Version="3.2.0" />
|
||||||
|
<PackageVersion Include="Diacritics" Version="3.3.14" />
|
||||||
|
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||||
|
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||||
|
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.8.3" />
|
||||||
|
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
|
||||||
|
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||||
|
<PackageVersion Include="libse" Version="3.6.10" />
|
||||||
|
<PackageVersion Include="LrcParser" Version="2022.529.1" />
|
||||||
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.3" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||||
|
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
||||||
|
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||||
|
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||||
|
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||||
|
<PackageVersion Include="NEbml" Version="0.11.0" />
|
||||||
|
<PackageVersion Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
|
<PackageVersion Include="PlaylistsNET" Version="1.3.1" />
|
||||||
|
<PackageVersion Include="prometheus-net.AspNetCore" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
||||||
|
<PackageVersion Include="prometheus-net" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="Serilog.AspNetCore" Version="6.1.0" />
|
||||||
|
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||||
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="3.4.0" />
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
|
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
|
<PackageVersion Include="SharpFuzz" Version="2.0.1" />
|
||||||
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
||||||
|
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||||
|
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
|
||||||
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
|
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
||||||
|
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
|
||||||
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.435" />
|
||||||
|
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
|
||||||
|
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||||
|
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||||
|
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||||
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="System.Text.Json" Version="7.0.2" />
|
||||||
|
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
|
||||||
|
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||||
|
<PackageVersion Include="TMDbLib" Version="2.0.0" />
|
||||||
|
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||||
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
|
||||||
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
|
<PackageVersion Include="xunit" Version="2.4.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
@ -37,7 +37,7 @@ RUN apt-get update \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
&& apt-get install --no-install-recommends --no-install-suggests -y \
|
||||||
mesa-va-drivers \
|
mesa-va-drivers \
|
||||||
jellyfin-ffmpeg \
|
jellyfin-ffmpeg5 \
|
||||||
openssl \
|
openssl \
|
||||||
locales \
|
locales \
|
||||||
# Intel VAAPI Tone mapping dependencies:
|
# Intel VAAPI Tone mapping dependencies:
|
||||||
|
@ -28,13 +28,13 @@
|
|||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -80,7 +80,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -7,6 +7,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Emby.Dlna.PlayTo;
|
using Emby.Dlna.PlayTo;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
@ -261,7 +262,7 @@ namespace Emby.Dlna.Main
|
|||||||
{
|
{
|
||||||
_publisher = new SsdpDevicePublisher(
|
_publisher = new SsdpDevicePublisher(
|
||||||
_communicationsServer,
|
_communicationsServer,
|
||||||
MediaBrowser.Common.System.OperatingSystem.Name,
|
Environment.OSVersion.Platform.ToString(),
|
||||||
Environment.OSVersion.VersionString,
|
Environment.OSVersion.VersionString,
|
||||||
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
_config.GetDlnaConfiguration().SendOnlyMatchedHost)
|
||||||
{
|
{
|
||||||
|
@ -42,18 +42,18 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using MediaBrowser.Controller.Notifications;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using MediaBrowser.Model.Notifications;
|
|
||||||
|
|
||||||
namespace Emby.Notifications
|
|
||||||
{
|
|
||||||
public class CoreNotificationTypes : INotificationTypeFactory
|
|
||||||
{
|
|
||||||
private readonly ILocalizationManager _localization;
|
|
||||||
|
|
||||||
public CoreNotificationTypes(ILocalizationManager localization)
|
|
||||||
{
|
|
||||||
_localization = localization;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<NotificationTypeInfo> GetNotificationTypes()
|
|
||||||
{
|
|
||||||
var knownTypes = new NotificationTypeInfo[]
|
|
||||||
{
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.ApplicationUpdateInstalled)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.InstallationFailed)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.PluginInstalled)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.PluginError)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.PluginUninstalled)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.PluginUpdateInstalled)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.ServerRestartRequired)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.TaskFailed)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.NewLibraryContent)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.AudioPlayback)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.VideoPlayback)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.AudioPlaybackStopped)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.VideoPlaybackStopped)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.UserLockedOut)
|
|
||||||
},
|
|
||||||
new NotificationTypeInfo
|
|
||||||
{
|
|
||||||
Type = nameof(NotificationType.ApplicationUpdateAvailable)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var type in knownTypes)
|
|
||||||
{
|
|
||||||
Update(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
var systemName = _localization.GetLocalizedString("System");
|
|
||||||
|
|
||||||
return knownTypes.OrderByDescending(i => string.Equals(i.Category, systemName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ThenBy(i => i.Category)
|
|
||||||
.ThenBy(i => i.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update(NotificationTypeInfo note)
|
|
||||||
{
|
|
||||||
note.Name = _localization.GetLocalizedString("NotificationOption" + note.Type);
|
|
||||||
|
|
||||||
note.IsBasedOnUserEvent = note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1;
|
|
||||||
|
|
||||||
if (note.Type.IndexOf("Playback", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("User");
|
|
||||||
}
|
|
||||||
else if (note.Type.IndexOf("Plugin", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("Plugin");
|
|
||||||
}
|
|
||||||
else if (note.Type.IndexOf("UserLockedOut", StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("User");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
note.Category = _localization.GetLocalizedString("System");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
|
||||||
<PropertyGroup>
|
|
||||||
<ProjectGuid>{2E030C33-6923-4530-9E54-FA29FA6AD1A9}</ProjectGuid>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Include="..\SharedVersion.cs" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
|
||||||
<ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<!-- Code analyzers-->
|
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,23 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Model.Notifications;
|
|
||||||
|
|
||||||
namespace Emby.Notifications
|
|
||||||
{
|
|
||||||
public class NotificationConfigurationFactory : IConfigurationFactory
|
|
||||||
{
|
|
||||||
public IEnumerable<ConfigurationStore> GetConfigurations()
|
|
||||||
{
|
|
||||||
return new ConfigurationStore[]
|
|
||||||
{
|
|
||||||
new ConfigurationStore
|
|
||||||
{
|
|
||||||
Key = "notifications",
|
|
||||||
ConfigurationType = typeof(NotificationOptions)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,314 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Events;
|
|
||||||
using Jellyfin.Extensions;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Controller;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Entities.Audio;
|
|
||||||
using MediaBrowser.Controller.Entities.TV;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Notifications;
|
|
||||||
using MediaBrowser.Controller.Plugins;
|
|
||||||
using MediaBrowser.Model.Activity;
|
|
||||||
using MediaBrowser.Model.Globalization;
|
|
||||||
using MediaBrowser.Model.Notifications;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Notifications
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates notifications for various system events.
|
|
||||||
/// </summary>
|
|
||||||
public class NotificationEntryPoint : IServerEntryPoint
|
|
||||||
{
|
|
||||||
private readonly ILogger<NotificationEntryPoint> _logger;
|
|
||||||
private readonly IActivityManager _activityManager;
|
|
||||||
private readonly ILocalizationManager _localization;
|
|
||||||
private readonly INotificationManager _notificationManager;
|
|
||||||
private readonly ILibraryManager _libraryManager;
|
|
||||||
private readonly IServerApplicationHost _appHost;
|
|
||||||
private readonly IConfigurationManager _config;
|
|
||||||
|
|
||||||
private readonly object _libraryChangedSyncLock = new object();
|
|
||||||
private readonly List<BaseItem> _itemsAdded = new List<BaseItem>();
|
|
||||||
|
|
||||||
private Timer? _libraryUpdateTimer;
|
|
||||||
|
|
||||||
private string[] _coreNotificationTypes;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="NotificationEntryPoint" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="activityManager">The activity manager.</param>
|
|
||||||
/// <param name="localization">The localization manager.</param>
|
|
||||||
/// <param name="notificationManager">The notification manager.</param>
|
|
||||||
/// <param name="libraryManager">The library manager.</param>
|
|
||||||
/// <param name="appHost">The application host.</param>
|
|
||||||
/// <param name="config">The configuration manager.</param>
|
|
||||||
public NotificationEntryPoint(
|
|
||||||
ILogger<NotificationEntryPoint> logger,
|
|
||||||
IActivityManager activityManager,
|
|
||||||
ILocalizationManager localization,
|
|
||||||
INotificationManager notificationManager,
|
|
||||||
ILibraryManager libraryManager,
|
|
||||||
IServerApplicationHost appHost,
|
|
||||||
IConfigurationManager config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_activityManager = activityManager;
|
|
||||||
_localization = localization;
|
|
||||||
_notificationManager = notificationManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_appHost = appHost;
|
|
||||||
_config = config;
|
|
||||||
|
|
||||||
_coreNotificationTypes = new CoreNotificationTypes(localization).GetNotificationTypes().Select(i => i.Type).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task RunAsync()
|
|
||||||
{
|
|
||||||
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
|
||||||
_appHost.HasPendingRestartChanged += OnAppHostHasPendingRestartChanged;
|
|
||||||
_activityManager.EntryCreated += OnActivityManagerEntryCreated;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnAppHostHasPendingRestartChanged(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
var type = NotificationType.ServerRestartRequired.ToString();
|
|
||||||
|
|
||||||
var notification = new NotificationRequest
|
|
||||||
{
|
|
||||||
NotificationType = type,
|
|
||||||
Name = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
_localization.GetLocalizedString("ServerNameNeedsToBeRestarted"),
|
|
||||||
_appHost.Name)
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendNotification(notification, null).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnActivityManagerEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
|
||||||
{
|
|
||||||
var entry = e.Argument;
|
|
||||||
|
|
||||||
var type = entry.Type;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(type) || !_coreNotificationTypes.Contains(type, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userId = e.Argument.UserId;
|
|
||||||
|
|
||||||
if (!userId.Equals(default) && !GetOptions().IsEnabledToMonitorUser(type, userId))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var notification = new NotificationRequest
|
|
||||||
{
|
|
||||||
NotificationType = type,
|
|
||||||
Name = entry.Name,
|
|
||||||
Description = entry.Overview
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendNotification(notification, null).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private NotificationOptions GetOptions()
|
|
||||||
{
|
|
||||||
return _config.GetConfiguration<NotificationOptions>("notifications");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
|
|
||||||
{
|
|
||||||
if (!FilterItem(e.Item))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_libraryChangedSyncLock)
|
|
||||||
{
|
|
||||||
if (_libraryUpdateTimer is null)
|
|
||||||
{
|
|
||||||
_libraryUpdateTimer = new Timer(
|
|
||||||
LibraryUpdateTimerCallback,
|
|
||||||
null,
|
|
||||||
5000,
|
|
||||||
Timeout.Infinite);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_libraryUpdateTimer.Change(5000, Timeout.Infinite);
|
|
||||||
}
|
|
||||||
|
|
||||||
_itemsAdded.Add(e.Item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool FilterItem(BaseItem item)
|
|
||||||
{
|
|
||||||
if (item.IsFolder)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.HasPathProtocol)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is IItemByName)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.SourceType == SourceType.Library;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void LibraryUpdateTimerCallback(object? state)
|
|
||||||
{
|
|
||||||
List<BaseItem> items;
|
|
||||||
|
|
||||||
lock (_libraryChangedSyncLock)
|
|
||||||
{
|
|
||||||
items = _itemsAdded.ToList();
|
|
||||||
_itemsAdded.Clear();
|
|
||||||
_libraryUpdateTimer!.Dispose(); // Shouldn't be null as it just set off this callback
|
|
||||||
_libraryUpdateTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.Count > 10)
|
|
||||||
{
|
|
||||||
items = items.GetRange(0, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in items)
|
|
||||||
{
|
|
||||||
var notification = new NotificationRequest
|
|
||||||
{
|
|
||||||
NotificationType = NotificationType.NewLibraryContent.ToString(),
|
|
||||||
Name = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
_localization.GetLocalizedString("ValueHasBeenAddedToLibrary"),
|
|
||||||
GetItemName(item)),
|
|
||||||
Description = item.Overview
|
|
||||||
};
|
|
||||||
|
|
||||||
await SendNotification(notification, item).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a human readable name for the item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The item.</param>
|
|
||||||
/// <returns>A human readable name for the item.</returns>
|
|
||||||
public static string GetItemName(BaseItem item)
|
|
||||||
{
|
|
||||||
var name = item.Name;
|
|
||||||
if (item is Episode episode)
|
|
||||||
{
|
|
||||||
if (episode.IndexNumber.HasValue)
|
|
||||||
{
|
|
||||||
name = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"Ep{0} - {1}",
|
|
||||||
episode.IndexNumber.Value,
|
|
||||||
name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (episode.ParentIndexNumber.HasValue)
|
|
||||||
{
|
|
||||||
name = string.Format(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
"S{0}, {1}",
|
|
||||||
episode.ParentIndexNumber.Value,
|
|
||||||
name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is IHasSeries hasSeries)
|
|
||||||
{
|
|
||||||
name = hasSeries.SeriesName + " - " + name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
|
||||||
{
|
|
||||||
var artists = hasAlbumArtist.AlbumArtists;
|
|
||||||
|
|
||||||
if (artists.Count > 0)
|
|
||||||
{
|
|
||||||
name = artists[0] + " - " + name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (item is IHasArtist hasArtist)
|
|
||||||
{
|
|
||||||
var artists = hasArtist.Artists;
|
|
||||||
|
|
||||||
if (artists.Count > 0)
|
|
||||||
{
|
|
||||||
name = artists[0] + " - " + name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendNotification(NotificationRequest notification, BaseItem? relatedItem)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _notificationManager.SendNotification(notification, relatedItem, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error sending notification");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Releases unmanaged and optionally managed resources.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_libraryUpdateTimer?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_libraryUpdateTimer = null;
|
|
||||||
|
|
||||||
_libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
|
|
||||||
_appHost.HasPendingRestartChanged -= OnAppHostHasPendingRestartChanged;
|
|
||||||
_activityManager.EntryCreated -= OnActivityManagerEntryCreated;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,224 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Data.Entities;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Controller.Configuration;
|
|
||||||
using MediaBrowser.Controller.Entities;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using MediaBrowser.Controller.Notifications;
|
|
||||||
using MediaBrowser.Model.Dto;
|
|
||||||
using MediaBrowser.Model.Notifications;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Emby.Notifications
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// NotificationManager class.
|
|
||||||
/// </summary>
|
|
||||||
public class NotificationManager : INotificationManager
|
|
||||||
{
|
|
||||||
private readonly ILogger<NotificationManager> _logger;
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly IServerConfigurationManager _config;
|
|
||||||
|
|
||||||
private INotificationService[] _services = Array.Empty<INotificationService>();
|
|
||||||
private INotificationTypeFactory[] _typeFactories = Array.Empty<INotificationTypeFactory>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="NotificationManager" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">The logger.</param>
|
|
||||||
/// <param name="userManager">The user manager.</param>
|
|
||||||
/// <param name="config">The server configuration manager.</param>
|
|
||||||
public NotificationManager(
|
|
||||||
ILogger<NotificationManager> logger,
|
|
||||||
IUserManager userManager,
|
|
||||||
IServerConfigurationManager config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_userManager = userManager;
|
|
||||||
_config = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
private NotificationOptions GetConfiguration()
|
|
||||||
{
|
|
||||||
return _config.GetConfiguration<NotificationOptions>("notifications");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task SendNotification(NotificationRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return SendNotification(request, null, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public Task SendNotification(NotificationRequest request, BaseItem? relatedItem, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var notificationType = request.NotificationType;
|
|
||||||
|
|
||||||
var options = string.IsNullOrEmpty(notificationType) ?
|
|
||||||
null :
|
|
||||||
GetConfiguration().GetOptions(notificationType);
|
|
||||||
|
|
||||||
var users = GetUserIds(request, options)
|
|
||||||
.Select(i => _userManager.GetUserById(i))
|
|
||||||
.Where(i => relatedItem is null || relatedItem.IsVisibleStandalone(i))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var title = request.Name;
|
|
||||||
var description = request.Description;
|
|
||||||
|
|
||||||
var tasks = _services.Where(i => IsEnabled(i, notificationType))
|
|
||||||
.Select(i => SendNotification(request, i, users, title, description, cancellationToken));
|
|
||||||
|
|
||||||
return Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SendNotification(
|
|
||||||
NotificationRequest request,
|
|
||||||
INotificationService service,
|
|
||||||
IEnumerable<User> users,
|
|
||||||
string title,
|
|
||||||
string description,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
users = users.Where(i => IsEnabledForUser(service, i));
|
|
||||||
|
|
||||||
var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
|
|
||||||
|
|
||||||
return Task.WhenAll(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<Guid> GetUserIds(NotificationRequest request, NotificationOption? options)
|
|
||||||
{
|
|
||||||
if (request.SendToUserMode.HasValue)
|
|
||||||
{
|
|
||||||
switch (request.SendToUserMode.Value)
|
|
||||||
{
|
|
||||||
case SendToUserType.Admins:
|
|
||||||
return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
|
|
||||||
.Select(i => i.Id);
|
|
||||||
case SendToUserType.All:
|
|
||||||
return _userManager.UsersIds;
|
|
||||||
case SendToUserType.Custom:
|
|
||||||
return request.UserIds;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException("Unrecognized SendToUserMode: " + request.SendToUserMode.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options is not null && !string.IsNullOrEmpty(request.NotificationType))
|
|
||||||
{
|
|
||||||
var config = GetConfiguration();
|
|
||||||
|
|
||||||
return _userManager.Users
|
|
||||||
.Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
|
|
||||||
.Select(i => i.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return request.UserIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SendNotification(
|
|
||||||
NotificationRequest request,
|
|
||||||
INotificationService service,
|
|
||||||
string title,
|
|
||||||
string description,
|
|
||||||
User user,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var notification = new UserNotification
|
|
||||||
{
|
|
||||||
Date = request.Date,
|
|
||||||
Description = description,
|
|
||||||
Level = request.Level,
|
|
||||||
Name = title,
|
|
||||||
Url = request.Url,
|
|
||||||
User = user
|
|
||||||
};
|
|
||||||
|
|
||||||
_logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await service.SendNotification(notification, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error sending notification to {0}", service.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsEnabledForUser(INotificationService service, User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return service.IsEnabledForUser(user);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in IsEnabledForUser");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsEnabled(INotificationService service, string notificationType)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(notificationType))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetConfiguration().IsServiceEnabled(service.Name, notificationType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void AddParts(IEnumerable<INotificationService> services, IEnumerable<INotificationTypeFactory> notificationTypeFactories)
|
|
||||||
{
|
|
||||||
_services = services.ToArray();
|
|
||||||
_typeFactories = notificationTypeFactories.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public List<NotificationTypeInfo> GetNotificationTypes()
|
|
||||||
{
|
|
||||||
var list = _typeFactories.Select(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return i.GetNotificationTypes().ToList();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in GetNotificationTypes");
|
|
||||||
return new List<NotificationTypeInfo>();
|
|
||||||
}
|
|
||||||
}).SelectMany(i => i).ToList();
|
|
||||||
|
|
||||||
var config = GetConfiguration();
|
|
||||||
|
|
||||||
foreach (var i in list)
|
|
||||||
{
|
|
||||||
i.Enabled = config.IsEnabled(i.Type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IEnumerable<NameIdPair> GetNotificationServices()
|
|
||||||
{
|
|
||||||
return _services.Select(i => new NameIdPair
|
|
||||||
{
|
|
||||||
Name = i.Name,
|
|
||||||
Id = i.Name.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
|
||||||
}).OrderBy(i => i.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
using System.Reflection;
|
|
||||||
using System.Resources;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
// General Information about an assembly is controlled through the following
|
|
||||||
// set of attributes. Change these attribute values to modify the information
|
|
||||||
// associated with an assembly.
|
|
||||||
[assembly: AssemblyTitle("Emby.Notifications")]
|
|
||||||
[assembly: AssemblyDescription("")]
|
|
||||||
[assembly: AssemblyConfiguration("")]
|
|
||||||
[assembly: AssemblyCompany("Jellyfin Project")]
|
|
||||||
[assembly: AssemblyProduct("Jellyfin Server")]
|
|
||||||
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
|
|
||||||
[assembly: AssemblyTrademark("")]
|
|
||||||
[assembly: AssemblyCulture("")]
|
|
||||||
[assembly: NeutralResourcesLanguage("en")]
|
|
||||||
|
|
||||||
// Setting ComVisible to false makes the types in this assembly not visible
|
|
||||||
// to COM components. If you need to access a type in this assembly from
|
|
||||||
// COM, set the ComVisible attribute to true on that type.
|
|
||||||
[assembly: ComVisible(false)]
|
|
@ -15,7 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@ -26,13 +26,13 @@
|
|||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The _configuration loaded.
|
|
||||||
/// </summary>
|
|
||||||
private bool _configurationLoaded;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The _configuration.
|
/// The _configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private BaseApplicationConfiguration _configuration;
|
private BaseApplicationConfiguration? _configuration;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
|
/// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
|
||||||
@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [configuration updated].
|
/// Occurs when [configuration updated].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<EventArgs> ConfigurationUpdated;
|
public event EventHandler<EventArgs>? ConfigurationUpdated;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [configuration updating].
|
/// Occurs when [configuration updating].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
|
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when [named configuration updated].
|
/// Occurs when [named configuration updated].
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
|
public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of the configuration.
|
/// Gets the type of the configuration.
|
||||||
@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (_configurationLoaded)
|
if (_configuration is not null)
|
||||||
{
|
{
|
||||||
return _configuration;
|
return _configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_configurationSyncLock)
|
lock (_configurationSyncLock)
|
||||||
{
|
{
|
||||||
if (_configurationLoaded)
|
if (_configuration is not null)
|
||||||
{
|
{
|
||||||
return _configuration;
|
return _configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
_configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
|
return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
|
||||||
|
|
||||||
_configurationLoaded = true;
|
|
||||||
|
|
||||||
return _configuration;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected set
|
protected set
|
||||||
{
|
{
|
||||||
_configuration = value;
|
_configuration = value;
|
||||||
|
|
||||||
_configurationLoaded = value is not null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
Logger.LogInformation("Saving system configuration");
|
Logger.LogInformation("Saving system configuration");
|
||||||
var path = CommonApplicationPaths.SystemConfigurationFilePath;
|
var path = CommonApplicationPaths.SystemConfigurationFilePath;
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||||
|
|
||||||
lock (_configurationSyncLock)
|
lock (_configurationSyncLock)
|
||||||
{
|
{
|
||||||
@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
|
|
||||||
private object LoadConfiguration(string path, Type configurationType)
|
private object LoadConfiguration(string path, Type configurationType)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
return Activator.CreateInstance(configurationType);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return XmlSerializer.DeserializeFromFile(configurationType, path);
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
return XmlSerializer.DeserializeFromFile(configurationType, path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (IOException)
|
catch (Exception ex) when (ex is not IOException)
|
||||||
{
|
|
||||||
return Activator.CreateInstance(configurationType);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
|
Logger.LogError(ex, "Error loading configuration file: {Path}", path);
|
||||||
|
|
||||||
return Activator.CreateInstance(configurationType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Activator.CreateInstance(configurationType)
|
||||||
|
?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||||||
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
|
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
|
||||||
|
|
||||||
var path = GetConfigurationFile(key);
|
var path = GetConfigurationFile(key);
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||||
|
|
||||||
lock (_configurationSyncLock)
|
lock (_configurationSyncLock)
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -19,7 +18,6 @@ using Emby.Dlna;
|
|||||||
using Emby.Dlna.Main;
|
using Emby.Dlna.Main;
|
||||||
using Emby.Dlna.Ssdp;
|
using Emby.Dlna.Ssdp;
|
||||||
using Emby.Naming.Common;
|
using Emby.Naming.Common;
|
||||||
using Emby.Notifications;
|
|
||||||
using Emby.Photos;
|
using Emby.Photos;
|
||||||
using Emby.Server.Implementations.Channels;
|
using Emby.Server.Implementations.Channels;
|
||||||
using Emby.Server.Implementations.Collections;
|
using Emby.Server.Implementations.Collections;
|
||||||
@ -70,7 +68,6 @@ using MediaBrowser.Controller.LiveTv;
|
|||||||
using MediaBrowser.Controller.Lyrics;
|
using MediaBrowser.Controller.Lyrics;
|
||||||
using MediaBrowser.Controller.MediaEncoding;
|
using MediaBrowser.Controller.MediaEncoding;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Notifications;
|
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Controller.Playlists;
|
using MediaBrowser.Controller.Playlists;
|
||||||
using MediaBrowser.Controller.Plugins;
|
using MediaBrowser.Controller.Plugins;
|
||||||
@ -115,15 +112,11 @@ namespace Emby.Server.Implementations
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The environment variable prefixes to log at server startup.
|
|
||||||
/// </summary>
|
|
||||||
private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The disposable parts.
|
/// The disposable parts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||||
|
private readonly DeviceId _deviceId;
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystemManager;
|
private readonly IFileSystem _fileSystemManager;
|
||||||
private readonly IConfiguration _startupConfig;
|
private readonly IConfiguration _startupConfig;
|
||||||
@ -132,7 +125,6 @@ namespace Emby.Server.Implementations
|
|||||||
private readonly IPluginManager _pluginManager;
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
private List<Type> _creatingInstances;
|
private List<Type> _creatingInstances;
|
||||||
private IMediaEncoder _mediaEncoder;
|
|
||||||
private ISessionManager _sessionManager;
|
private ISessionManager _sessionManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -141,8 +133,6 @@ namespace Emby.Server.Implementations
|
|||||||
/// <value>All concrete types.</value>
|
/// <value>All concrete types.</value>
|
||||||
private Type[] _allConcreteTypes;
|
private Type[] _allConcreteTypes;
|
||||||
|
|
||||||
private DeviceId _deviceId;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
private bool _disposed = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -166,6 +156,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||||
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
|
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
|
||||||
|
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||||
|
|
||||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||||
ApplicationVersionString = ApplicationVersion.ToString(3);
|
ApplicationVersionString = ApplicationVersion.ToString(3);
|
||||||
@ -193,23 +184,9 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
public bool CoreStartupHasCompleted { get; private set; }
|
public bool CoreStartupHasCompleted { get; private set; }
|
||||||
|
|
||||||
public virtual bool CanLaunchWebBrowser
|
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||||
{
|
&& !_startupOptions.IsService
|
||||||
get
|
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||||
{
|
|
||||||
if (!Environment.UserInteractive)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_startupOptions.IsService)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||||
@ -286,15 +263,7 @@ namespace Emby.Server.Implementations
|
|||||||
/// <value>The application name.</value>
|
/// <value>The application name.</value>
|
||||||
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
|
||||||
|
|
||||||
public string SystemId
|
public string SystemId => _deviceId.Value;
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
_deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
|
|
||||||
|
|
||||||
return _deviceId.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public string Name => ApplicationProductName;
|
public string Name => ApplicationProductName;
|
||||||
@ -447,7 +416,7 @@ namespace Emby.Server.Implementations
|
|||||||
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
|
||||||
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
|
||||||
|
|
||||||
_mediaEncoder.SetFFmpegPath();
|
Resolve<IMediaEncoder>().SetFFmpegPath();
|
||||||
|
|
||||||
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
||||||
|
|
||||||
@ -615,8 +584,6 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||||
@ -659,7 +626,6 @@ namespace Emby.Server.Implementations
|
|||||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||||
|
|
||||||
_mediaEncoder = Resolve<IMediaEncoder>();
|
|
||||||
_sessionManager = Resolve<ISessionManager>();
|
_sessionManager = Resolve<ISessionManager>();
|
||||||
|
|
||||||
SetStaticProperties();
|
SetStaticProperties();
|
||||||
@ -670,36 +636,6 @@ namespace Emby.Server.Implementations
|
|||||||
FindParts();
|
FindParts();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
|
|
||||||
{
|
|
||||||
// Distinct these to prevent users from reporting problems that aren't actually problems
|
|
||||||
var commandLineArgs = Environment
|
|
||||||
.GetCommandLineArgs()
|
|
||||||
.Distinct();
|
|
||||||
|
|
||||||
// Get all relevant environment variables
|
|
||||||
var allEnvVars = Environment.GetEnvironmentVariables();
|
|
||||||
var relevantEnvVars = new Dictionary<object, object>();
|
|
||||||
foreach (var key in allEnvVars.Keys)
|
|
||||||
{
|
|
||||||
if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
relevantEnvVars.Add(key, allEnvVars[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
|
|
||||||
logger.LogInformation("Arguments: {Args}", commandLineArgs);
|
|
||||||
logger.LogInformation("Operating system: {OS}", MediaBrowser.Common.System.OperatingSystem.Name);
|
|
||||||
logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
|
|
||||||
logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
|
|
||||||
logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
|
|
||||||
logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
|
|
||||||
logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
|
|
||||||
logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
|
|
||||||
logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private X509Certificate2 GetCertificate(string path, string password)
|
private X509Certificate2 GetCertificate(string path, string password)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@ -786,13 +722,7 @@ namespace Emby.Server.Implementations
|
|||||||
|
|
||||||
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
|
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
|
||||||
|
|
||||||
Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
|
|
||||||
|
|
||||||
Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
|
|
||||||
|
|
||||||
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
||||||
|
|
||||||
Resolve<INotificationManager>().AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -991,9 +921,6 @@ namespace Emby.Server.Implementations
|
|||||||
// Local metadata
|
// Local metadata
|
||||||
yield return typeof(BoxSetXmlSaver).Assembly;
|
yield return typeof(BoxSetXmlSaver).Assembly;
|
||||||
|
|
||||||
// Notifications
|
|
||||||
yield return typeof(NotificationManager).Assembly;
|
|
||||||
|
|
||||||
// Xbmc
|
// Xbmc
|
||||||
yield return typeof(ArtistNfoProvider).Assembly;
|
yield return typeof(ArtistNfoProvider).Assembly;
|
||||||
|
|
||||||
@ -1032,14 +959,11 @@ namespace Emby.Server.Implementations
|
|||||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||||
CachePath = ApplicationPaths.CachePath,
|
CachePath = ApplicationPaths.CachePath,
|
||||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
|
||||||
OperatingSystemDisplayName = MediaBrowser.Common.System.OperatingSystem.Name,
|
|
||||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
LocalAddress = GetSmartApiUrl(request),
|
||||||
SupportsLibraryMonitor = true,
|
SupportsLibraryMonitor = true,
|
||||||
SystemArchitecture = RuntimeInformation.OSArchitecture,
|
|
||||||
PackageName = _startupOptions.PackageName
|
PackageName = _startupOptions.PackageName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1051,7 +975,6 @@ namespace Emby.Server.Implementations
|
|||||||
Version = ApplicationVersionString,
|
Version = ApplicationVersionString,
|
||||||
ProductName = ApplicationProductName,
|
ProductName = ApplicationProductName,
|
||||||
Id = SystemId,
|
Id = SystemId,
|
||||||
OperatingSystem = MediaBrowser.Common.System.OperatingSystem.Id.ToString(),
|
|
||||||
ServerName = FriendlyName,
|
ServerName = FriendlyName,
|
||||||
LocalAddress = GetSmartApiUrl(request),
|
LocalAddress = GetSmartApiUrl(request),
|
||||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||||
@ -1263,10 +1186,13 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// used for closing websockets
|
if (_sessionManager != null)
|
||||||
foreach (var session in _sessionManager.Sessions)
|
|
||||||
{
|
{
|
||||||
await session.DisposeAsync().ConfigureAwait(false);
|
// used for closing websockets
|
||||||
|
foreach (var session in _sessionManager.Sessions)
|
||||||
|
{
|
||||||
|
await session.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
/// <param name="userDataManager">The user data manager.</param>
|
/// <param name="userDataManager">The user data manager.</param>
|
||||||
/// <param name="providerManager">The provider manager.</param>
|
/// <param name="providerManager">The provider manager.</param>
|
||||||
/// <param name="memoryCache">The memory cache.</param>
|
/// <param name="memoryCache">The memory cache.</param>
|
||||||
|
/// <param name="channels">The channels.</param>
|
||||||
public ChannelManager(
|
public ChannelManager(
|
||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IDtoService dtoService,
|
IDtoService dtoService,
|
||||||
@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
IFileSystem fileSystem,
|
IFileSystem fileSystem,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
IProviderManager providerManager,
|
IProviderManager providerManager,
|
||||||
IMemoryCache memoryCache)
|
IMemoryCache memoryCache,
|
||||||
|
IEnumerable<IChannel> channels)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_dtoService = dtoService;
|
_dtoService = dtoService;
|
||||||
@ -86,18 +88,13 @@ namespace Emby.Server.Implementations.Channels
|
|||||||
_userDataManager = userDataManager;
|
_userDataManager = userDataManager;
|
||||||
_providerManager = providerManager;
|
_providerManager = providerManager;
|
||||||
_memoryCache = memoryCache;
|
_memoryCache = memoryCache;
|
||||||
}
|
|
||||||
|
|
||||||
internal IChannel[] Channels { get; private set; }
|
|
||||||
|
|
||||||
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void AddParts(IEnumerable<IChannel> channels)
|
|
||||||
{
|
|
||||||
Channels = channels.ToArray();
|
Channels = channels.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal IChannel[] Channels { get; }
|
||||||
|
|
||||||
|
private static TimeSpan CacheLength => TimeSpan.FromHours(3);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool EnableMediaSourceDisplay(BaseItem item)
|
public bool EnableMediaSourceDisplay(BaseItem item)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration updating event.
|
/// Configuration updating event.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
|
public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the type of the configuration.
|
/// Gets the type of the configuration.
|
||||||
|
@ -4477,6 +4477,24 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.IncludeInheritedTags.Length > 0)
|
||||||
|
{
|
||||||
|
var paramName = "@IncludeInheritedTags";
|
||||||
|
if (statement is null)
|
||||||
|
{
|
||||||
|
int index = 0;
|
||||||
|
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
|
||||||
|
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int index = 0; index < query.IncludeInheritedTags.Length; index++)
|
||||||
|
{
|
||||||
|
statement.TryBind(paramName + index, GetCleanValue(query.IncludeInheritedTags[index]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (query.SeriesStatuses.Length > 0)
|
if (query.SeriesStatuses.Length > 0)
|
||||||
{
|
{
|
||||||
var statuses = new List<string>();
|
var statuses = new List<string>();
|
||||||
@ -5440,6 +5458,9 @@ AND Type = @InternalPersonType)");
|
|||||||
|
|
||||||
list.AddRange(inheritedTags.Select(i => (6, i)));
|
list.AddRange(inheritedTags.Select(i => (6, i)));
|
||||||
|
|
||||||
|
// Remove all invalid values.
|
||||||
|
list.RemoveAll(i => string.IsNullOrEmpty(i.Item2));
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
<ProjectReference Include="..\Emby.Naming\Emby.Naming.csproj" />
|
||||||
<ProjectReference Include="..\Emby.Notifications\Emby.Notifications.csproj" />
|
|
||||||
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
<ProjectReference Include="..\Jellyfin.Api\Jellyfin.Api.csproj" />
|
||||||
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
<ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
@ -23,17 +22,17 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
|
<PackageReference Include="DiscUtils.Udf" />
|
||||||
<PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
|
<PackageReference Include="Jellyfin.XmlTv" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
<PackageReference Include="Mono.Nat" Version="3.0.4" />
|
<PackageReference Include="Mono.Nat" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
<PackageReference Include="SQLitePCL.pretty.netstandard" />
|
||||||
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
|
<PackageReference Include="DotNet.Glob" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -54,13 +53,13 @@
|
|||||||
|
|
||||||
<!-- Code Analyzers-->
|
<!-- Code Analyzers-->
|
||||||
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
|
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
|
||||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
|
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
|
||||||
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
|
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
|
|
||||||
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
|
private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
|
||||||
{
|
{
|
||||||
string episodeTitle = program.Episode?.Title;
|
string episodeTitle = program.Episode.Title;
|
||||||
|
var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
|
||||||
|
|
||||||
var programInfo = new ProgramInfo
|
var programInfo = new ProgramInfo
|
||||||
{
|
{
|
||||||
ChannelId = program.ChannelId,
|
ChannelId = program.ChannelId,
|
||||||
EndDate = program.EndDate.UtcDateTime,
|
EndDate = program.EndDate.UtcDateTime,
|
||||||
EpisodeNumber = program.Episode?.Episode,
|
EpisodeNumber = program.Episode.Episode,
|
||||||
EpisodeTitle = episodeTitle,
|
EpisodeTitle = episodeTitle,
|
||||||
Genres = program.Categories,
|
Genres = programCategories,
|
||||||
StartDate = program.StartDate.UtcDateTime,
|
StartDate = program.StartDate.UtcDateTime,
|
||||||
Name = program.Title,
|
Name = program.Title,
|
||||||
Overview = program.Description,
|
Overview = program.Description,
|
||||||
ProductionYear = program.CopyrightDate?.Year,
|
ProductionYear = program.CopyrightDate?.Year,
|
||||||
SeasonNumber = program.Episode?.Series,
|
SeasonNumber = program.Episode.Series,
|
||||||
IsSeries = program.Episode is not null,
|
IsSeries = program.Episode.Series is not null,
|
||||||
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
IsRepeat = program.IsPreviouslyShown && !program.IsNew,
|
||||||
IsPremiere = program.Premiere is not null,
|
IsPremiere = program.Premiere is not null,
|
||||||
IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||||
IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||||
IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||||
IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
|
||||||
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
|
ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
|
||||||
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
|
HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
|
||||||
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
|
OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
|
||||||
CommunityRating = program.StarRating,
|
CommunityRating = program.StarRating,
|
||||||
SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(program.ProgramId))
|
if (string.IsNullOrWhiteSpace(program.ProgramId))
|
||||||
@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||||||
{
|
{
|
||||||
Id = c.Id,
|
Id = c.Id,
|
||||||
Name = c.DisplayName,
|
Name = c.DisplayName,
|
||||||
ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
|
ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
|
||||||
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
|
Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
|
using Jellyfin.Extensions.Json.Converters;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
_socketFactory = socketFactory;
|
_socketFactory = socketFactory;
|
||||||
_streamHelper = streamHelper;
|
_streamHelper = streamHelper;
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.Options;
|
_jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
|
||||||
|
_jsonOptions.Converters.Add(new JsonBoolNumberConverter());
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Name => "HD Homerun";
|
public string Name => "HD Homerun";
|
||||||
|
@ -1,4 +1,127 @@
|
|||||||
{
|
{
|
||||||
"Sync": "Сінхранізацыя",
|
"Sync": "Сінхранізаваць",
|
||||||
"Playlists": "Плэйліст"
|
"Playlists": "Плэйлісты",
|
||||||
|
"Latest": "Апошні",
|
||||||
|
"LabelIpAddressValue": "IP-адрас: {0}",
|
||||||
|
"ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
|
||||||
|
"MessageApplicationUpdated": "Сервер Jellyfin абноўлены",
|
||||||
|
"NotificationOptionApplicationUpdateInstalled": "Абнаўленне прыкладання ўсталявана",
|
||||||
|
"PluginInstalledWithName": "{0} быў усталяваны",
|
||||||
|
"UserCreatedWithName": "Карыстальнік {0} быў створаны",
|
||||||
|
"Albums": "Альбомы",
|
||||||
|
"Application": "Прыкладанне",
|
||||||
|
"AuthenticationSucceededWithUserName": "{0} паспяхова аўтэнтыфікаваны",
|
||||||
|
"Channels": "Каналы",
|
||||||
|
"ChapterNameValue": "Раздзел {0}",
|
||||||
|
"Collections": "Калекцыі",
|
||||||
|
"Default": "Па змаўчанні",
|
||||||
|
"FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
|
||||||
|
"Folders": "Папкі",
|
||||||
|
"Favorites": "Абранае",
|
||||||
|
"External": "Знешні",
|
||||||
|
"Genres": "Жанры",
|
||||||
|
"HeaderContinueWatching": "Працягнуць прагляд",
|
||||||
|
"HeaderFavoriteAlbums": "Абраныя альбомы",
|
||||||
|
"HeaderFavoriteEpisodes": "Абраныя серыі",
|
||||||
|
"HeaderFavoriteShows": "Абраныя шоу",
|
||||||
|
"HeaderFavoriteSongs": "Абраныя песні",
|
||||||
|
"HeaderLiveTV": "Прамы эфір",
|
||||||
|
"HeaderAlbumArtists": "Выканаўцы альбома",
|
||||||
|
"LabelRunningTimeValue": "Працягласць: {0}",
|
||||||
|
"HomeVideos": "Хатнія відэа",
|
||||||
|
"ItemRemovedWithName": "{0} быў выдалены з бібліятэкі",
|
||||||
|
"MessageApplicationUpdatedTo": "Сервер Jellyfin абноўлены да {0}",
|
||||||
|
"Movies": "Фільмы",
|
||||||
|
"Music": "Музыка",
|
||||||
|
"MusicVideos": "Музычныя кліпы",
|
||||||
|
"NameInstallFailed": "Устаноўка {0} не атрымалася",
|
||||||
|
"NameSeasonNumber": "Сезон {0}",
|
||||||
|
"NotificationOptionApplicationUpdateAvailable": "Даступна абнаўленне прыкладання",
|
||||||
|
"NotificationOptionPluginInstalled": "Плагін усталяваны",
|
||||||
|
"NotificationOptionPluginUpdateInstalled": "Абнаўленне плагіна усталявана",
|
||||||
|
"NotificationOptionServerRestartRequired": "Патрабуецца перазапуск сервера",
|
||||||
|
"Photos": "Фатаграфіі",
|
||||||
|
"Plugin": "Плагін",
|
||||||
|
"PluginUninstalledWithName": "{0} быў выдалены",
|
||||||
|
"PluginUpdatedWithName": "{0} быў абноўлены",
|
||||||
|
"ProviderValue": "Пастаўшчык: {0}",
|
||||||
|
"Songs": "Песні",
|
||||||
|
"System": "Сістэма",
|
||||||
|
"User": "Карыстальнік",
|
||||||
|
"UserDeletedWithName": "Карыстальнік {0} быў выдалены",
|
||||||
|
"UserDownloadingItemWithValues": "{0} спампоўваецца {1}",
|
||||||
|
"TaskOptimizeDatabase": "Аптымізаваць базу дадзеных",
|
||||||
|
"Artists": "Выканаўцы",
|
||||||
|
"UserOfflineFromDevice": "{0} адключыўся ад {1}",
|
||||||
|
"UserPolicyUpdatedWithName": "Палітыка карыстальніка абноўлена для {0}",
|
||||||
|
"TaskCleanActivityLogDescription": "Выдаляе старэйшыя за зададзены ўзрост запісы ў журнале актыўнасці.",
|
||||||
|
"TaskRefreshChapterImagesDescription": "Стварае мініяцюры для відэа, якія маюць раздзелы.",
|
||||||
|
"TaskCleanLogsDescription": "Выдаляе файлы журналу, якім больш за {0} дзён.",
|
||||||
|
"TaskUpdatePluginsDescription": "Спампоўвае і ўсталёўвае абнаўленні для плагінаў, якія настроены на аўтаматычнае абнаўленне.",
|
||||||
|
"TaskRefreshChannelsDescription": "Абнаўляе інфармацыю аб інтэрнэт-канале.",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Шукае ў інтэрнэце адсутныя субтытры на аснове канфігурацыі метададзеных.",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Ушчыльняе базу дадзеных і скарачае вольную прастору. Выкананне гэтай задачы пасля сканавання бібліятэкі або ўнясення іншых змяненняў, якія прадугледжваюць мадыфікацыю базы дадзеных, можа палепшыць прадукцыйнасць.",
|
||||||
|
"TaskKeyframeExtractor": "Экстрактар ключавых кадраў",
|
||||||
|
"TasksApplicationCategory": "Прыкладанне",
|
||||||
|
"AppDeviceValues": "Прыкладанне: {0}, Прылада: {1}",
|
||||||
|
"Books": "Кнігі",
|
||||||
|
"CameraImageUploadedFrom": "Новая выява камеры была загружана з {0}",
|
||||||
|
"DeviceOfflineWithName": "{0} адключыўся",
|
||||||
|
"DeviceOnlineWithName": "{0} падлучаны",
|
||||||
|
"Forced": "Прымусова",
|
||||||
|
"HeaderRecordingGroups": "Групы запісаў",
|
||||||
|
"HeaderNextUp": "Наступнае",
|
||||||
|
"HeaderFavoriteArtists": "Абраныя выканаўцы",
|
||||||
|
"HearingImpaired": "Са слабым слыхам",
|
||||||
|
"Inherit": "Атрымаць у спадчыну",
|
||||||
|
"MessageNamedServerConfigurationUpdatedWithValue": "Канфігурацыя сервера {0} абноўлена",
|
||||||
|
"MessageServerConfigurationUpdated": "Канфігурацыя сервера абноўлена",
|
||||||
|
"MixedContent": "Змешаны змест",
|
||||||
|
"NameSeasonUnknown": "Невядомы сезон",
|
||||||
|
"NotificationOptionInstallationFailed": "Збой усталёўкі",
|
||||||
|
"NewVersionIsAvailable": "Новая версія сервера Jellyfin даступная для cпампоўкі.",
|
||||||
|
"NotificationOptionCameraImageUploaded": "Выява камеры запампавана",
|
||||||
|
"NotificationOptionAudioPlaybackStopped": "Прайграванне аўдыё спынена",
|
||||||
|
"NotificationOptionAudioPlayback": "Прайграванне аўдыё пачалося",
|
||||||
|
"NotificationOptionNewLibraryContent": "Дададзены новы кантэнт",
|
||||||
|
"NotificationOptionPluginError": "Збой плагіна",
|
||||||
|
"NotificationOptionPluginUninstalled": "Плагін выдалены",
|
||||||
|
"NotificationOptionTaskFailed": "Збой запланаванага задання",
|
||||||
|
"NotificationOptionUserLockedOut": "Карыстальнік заблакіраваны",
|
||||||
|
"NotificationOptionVideoPlayback": "Пачалося прайграванне відэа",
|
||||||
|
"NotificationOptionVideoPlaybackStopped": "Прайграванне відэа спынена",
|
||||||
|
"ScheduledTaskFailedWithName": "{0} не атрымалася",
|
||||||
|
"ScheduledTaskStartedWithName": "{0} пачалося",
|
||||||
|
"ServerNameNeedsToBeRestarted": "{0} трэба перазапусціць",
|
||||||
|
"Shows": "Шоу",
|
||||||
|
"StartupEmbyServerIsLoading": "Jellyfin Server загружаецца. Калі ласка, паўтарыце спробу крыху пазней.",
|
||||||
|
"SubtitleDownloadFailureFromForItem": "Не атрымалася спампаваць субтытры з {0} для {1}",
|
||||||
|
"TvShows": "ТБ-шоу",
|
||||||
|
"Undefined": "Нявызначана",
|
||||||
|
"UserLockedOutWithName": "Карыстальнік {0} быў заблакіраваны",
|
||||||
|
"UserOnlineFromDevice": "{0} падключаны з {1}",
|
||||||
|
"UserPasswordChangedWithName": "Пароль быў зменены для карыстальніка {0}",
|
||||||
|
"UserStartedPlayingItemWithValues": "{0} грае {1} на {2}",
|
||||||
|
"UserStoppedPlayingItemWithValues": "{0} скончыў прайграванне {1} на {2}",
|
||||||
|
"ValueHasBeenAddedToLibrary": "{0} быў дададзены ў вашу медыятэку",
|
||||||
|
"ValueSpecialEpisodeName": "Спецэпізод - {0}",
|
||||||
|
"VersionNumber": "Версія {0}",
|
||||||
|
"TasksMaintenanceCategory": "Абслугоўванне",
|
||||||
|
"TasksLibraryCategory": "Медыятэка",
|
||||||
|
"TasksChannelsCategory": "Інтэрнэт-каналы",
|
||||||
|
"TaskCleanActivityLog": "Ачысціць журнал актыўнасці",
|
||||||
|
"TaskCleanCache": "Ачысціць кэш",
|
||||||
|
"TaskCleanCacheDescription": "Выдаляе файлы кэша, якія больш не патрэбныя сістэме.",
|
||||||
|
"TaskRefreshChapterImages": "Выняць выявы раздзелаў",
|
||||||
|
"TaskRefreshLibrary": "Сканіраваць медыятэку",
|
||||||
|
"TaskRefreshLibraryDescription": "Сканіруе вашу медыятэку на наяўнасць новых файлаў і абнаўляе метададзеныя.",
|
||||||
|
"TaskCleanLogs": "Ачысціць часопіс",
|
||||||
|
"TaskRefreshPeople": "Абнавіць людзей",
|
||||||
|
"TaskRefreshPeopleDescription": "Абнаўленне метаданых для акцёраў і рэжысёраў у вашай медыятэцы.",
|
||||||
|
"TaskUpdatePlugins": "Абнавіць плагіны",
|
||||||
|
"TaskCleanTranscode": "Ачысціць каталог перакадзіравання",
|
||||||
|
"TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
|
||||||
|
"TaskRefreshChannels": "Абнавіць каналы",
|
||||||
|
"TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
|
||||||
|
"TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
|
||||||
}
|
}
|
||||||
|
@ -122,5 +122,6 @@
|
|||||||
"TaskOptimizeDatabase": "Optimizar base de datos",
|
"TaskOptimizeDatabase": "Optimizar base de datos",
|
||||||
"External": "Externo",
|
"External": "Externo",
|
||||||
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.",
|
||||||
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave"
|
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
|
||||||
|
"HearingImpaired": "Discapacidad auditiva"
|
||||||
}
|
}
|
||||||
|
@ -123,5 +123,6 @@
|
|||||||
"TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
|
"TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
|
||||||
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
|
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
|
||||||
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
|
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
|
||||||
"External": "خارجی"
|
"External": "خارجی",
|
||||||
|
"HearingImpaired": "مشکل شنوایی"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"Albums": "Alben",
|
"Albums": "Alben",
|
||||||
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
"AppDeviceValues": "App: {0}, Gerät: {1}",
|
||||||
"Application": "Anwendung",
|
"Application": "Applikation",
|
||||||
"Artists": "Künstler",
|
"Artists": "Künstler",
|
||||||
"AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
|
"AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
|
||||||
"Books": "Bücher",
|
"Books": "Bücher",
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
"FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
|
||||||
"Favorites": "Favoriten",
|
"Favorites": "Favoriten",
|
||||||
"Folders": "Ordner",
|
"Folders": "Ordner",
|
||||||
"Genres": "Genres",
|
"Genres": "Genre",
|
||||||
"HeaderAlbumArtists": "Album-Künstler",
|
"HeaderAlbumArtists": "Album-Künstler",
|
||||||
"HeaderContinueWatching": "weiter schauen",
|
"HeaderContinueWatching": "weiter schauen",
|
||||||
"HeaderFavoriteAlbums": "Lieblingsalben",
|
"HeaderFavoriteAlbums": "Lieblingsalben",
|
||||||
@ -49,7 +49,7 @@
|
|||||||
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
|
"NotificationOptionAudioPlayback": "Audiowedergab gstartet",
|
||||||
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
|
"NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
|
||||||
"NotificationOptionCameraImageUploaded": "Foti ueglade",
|
"NotificationOptionCameraImageUploaded": "Foti ueglade",
|
||||||
"NotificationOptionInstallationFailed": "Installationsfehler",
|
"NotificationOptionInstallationFailed": "Installationsfähler",
|
||||||
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
|
"NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
|
||||||
"NotificationOptionPluginError": "Plugin-Fäuer",
|
"NotificationOptionPluginError": "Plugin-Fäuer",
|
||||||
"NotificationOptionPluginInstalled": "Plugin installiert",
|
"NotificationOptionPluginInstalled": "Plugin installiert",
|
||||||
@ -120,5 +120,9 @@
|
|||||||
"Forced": "Erzwungen",
|
"Forced": "Erzwungen",
|
||||||
"Default": "Standard",
|
"Default": "Standard",
|
||||||
"TaskOptimizeDatabase": "Datenbank optimieren",
|
"TaskOptimizeDatabase": "Datenbank optimieren",
|
||||||
"External": "Extern"
|
"External": "Extern",
|
||||||
|
"TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.",
|
||||||
|
"HearingImpaired": "Hörgschädigti",
|
||||||
|
"TaskKeyframeExtractor": "Keyframe-Extraktor",
|
||||||
|
"TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe."
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,10 @@
|
|||||||
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
|
"FailedLoginAttemptWithUserName": "Ye failed to get in, try from {0}",
|
||||||
"Favorites": "Finest Loot",
|
"Favorites": "Finest Loot",
|
||||||
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
"ItemRemovedWithName": "{0} was taken from yer treasure",
|
||||||
"LabelIpAddressValue": "Ship's coordinates: {0}"
|
"LabelIpAddressValue": "Ship's coordinates: {0}",
|
||||||
|
"Genres": "types o' booty",
|
||||||
|
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
|
||||||
|
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
|
||||||
|
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
|
||||||
|
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
"PluginInstalledWithName": "{0} installerades",
|
"PluginInstalledWithName": "{0} installerades",
|
||||||
"PluginUninstalledWithName": "{0} avinstallerades",
|
"PluginUninstalledWithName": "{0} avinstallerades",
|
||||||
"PluginUpdatedWithName": "{0} uppdaterades",
|
"PluginUpdatedWithName": "{0} uppdaterades",
|
||||||
"ProviderValue": "Källa: {0}",
|
"ProviderValue": "Leverantör: {0}",
|
||||||
"ScheduledTaskFailedWithName": "{0} misslyckades",
|
"ScheduledTaskFailedWithName": "{0} misslyckades",
|
||||||
"ScheduledTaskStartedWithName": "{0} startades",
|
"ScheduledTaskStartedWithName": "{0} startades",
|
||||||
"ServerNameNeedsToBeRestarted": "{0} behöver startas om",
|
"ServerNameNeedsToBeRestarted": "{0} behöver startas om",
|
||||||
|
@ -123,41 +123,64 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var assemblyLoadContext = new PluginLoadContext(plugin.Path);
|
||||||
|
_assemblyLoadContexts.Add(assemblyLoadContext);
|
||||||
|
|
||||||
|
var assemblies = new List<Assembly>(plugin.DllFiles.Count);
|
||||||
|
var loadedAll = true;
|
||||||
|
|
||||||
foreach (var file in plugin.DllFiles)
|
foreach (var file in plugin.DllFiles)
|
||||||
{
|
{
|
||||||
Assembly assembly;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var assemblyLoadContext = new PluginLoadContext(file);
|
assemblies.Add(assemblyLoadContext.LoadFromAssemblyPath(file));
|
||||||
_assemblyLoadContexts.Add(assemblyLoadContext);
|
|
||||||
|
|
||||||
assembly = assemblyLoadContext.LoadFromAssemblyPath(file);
|
|
||||||
|
|
||||||
// Load all required types to verify that the plugin will load
|
|
||||||
assembly.GetTypes();
|
|
||||||
}
|
}
|
||||||
catch (FileLoadException ex)
|
catch (FileLoadException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin.", file);
|
_logger.LogError(ex, "Failed to load assembly {Path}. Disabling plugin", file);
|
||||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
continue;
|
loadedAll = false;
|
||||||
}
|
break;
|
||||||
catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // 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
|
#pragma warning disable CA1031 // Do not catch general exception types
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
#pragma warning restore CA1031 // Do not catch general exception types
|
#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);
|
_logger.LogError(ex, "Failed to load assembly {Path}. Unknown exception was thrown. Disabling plugin", file);
|
||||||
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
continue;
|
loadedAll = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loadedAll)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Load all required types to verify that the plugin will load
|
||||||
|
assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // 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", assembly.Location);
|
||||||
|
ChangePluginState(plugin, PluginStatus.NotSupported);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
#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", assembly.Location);
|
||||||
|
ChangePluginState(plugin, PluginStatus.Malfunctioned);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, file);
|
_logger.LogInformation("Loaded assembly {Assembly} from {Path}", assembly.FullName, assembly.Location);
|
||||||
yield return assembly;
|
yield return assembly;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,28 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal produces image attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class AcceptsFileAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
private readonly string[] _contentTypes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal produces image attribute.
|
/// Initializes a new instance of the <see cref="AcceptsFileAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
public class AcceptsFileAttribute : Attribute
|
public AcceptsFileAttribute(params string[] contentTypes)
|
||||||
{
|
{
|
||||||
private readonly string[] _contentTypes;
|
_contentTypes = 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[] ContentTypes => _contentTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configured content types.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>the configured content types.</returns>
|
||||||
|
public string[] ContentTypes => _contentTypes;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "image/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AcceptsImageFileAttribute()
|
public sealed class AcceptsImageFileAttribute : AcceptsFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
private const string ContentType = "image/*";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AcceptsImageFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public AcceptsImageFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,28 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that supports the HTTP GET method.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HttpSubscribeAttribute : HttpMethodAttribute
|
||||||
{
|
{
|
||||||
|
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identifies an action that supports the HTTP GET method.
|
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class HttpSubscribeAttribute : HttpMethodAttribute
|
public HttpSubscribeAttribute()
|
||||||
|
: base(_supportedMethods)
|
||||||
{
|
{
|
||||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "SUBSCRIBE" };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public HttpSubscribeAttribute()
|
|
||||||
: base(_supportedMethods)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="template">The route template. May not be null.</param>
|
|
||||||
public HttpSubscribeAttribute(string template)
|
|
||||||
: base(_supportedMethods, template)
|
|
||||||
=> ArgumentNullException.ThrowIfNull(template);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpSubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public HttpSubscribeAttribute(string template)
|
||||||
|
: base(_supportedMethods, template)
|
||||||
|
=> ArgumentNullException.ThrowIfNull(template);
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,28 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.AspNetCore.Mvc.Routing;
|
using Microsoft.AspNetCore.Mvc.Routing;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies an action that supports the HTTP GET method.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
|
||||||
{
|
{
|
||||||
|
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identifies an action that supports the HTTP GET method.
|
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class HttpUnsubscribeAttribute : HttpMethodAttribute
|
public HttpUnsubscribeAttribute()
|
||||||
|
: base(_supportedMethods)
|
||||||
{
|
{
|
||||||
private static readonly IEnumerable<string> _supportedMethods = new[] { "UNSUBSCRIBE" };
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
|
||||||
/// </summary>
|
|
||||||
public HttpUnsubscribeAttribute()
|
|
||||||
: base(_supportedMethods)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="template">The route template. May not be null.</param>
|
|
||||||
public HttpUnsubscribeAttribute(string template)
|
|
||||||
: base(_supportedMethods, template)
|
|
||||||
=> ArgumentNullException.ThrowIfNull(template);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="HttpUnsubscribeAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The route template. May not be null.</param>
|
||||||
|
public HttpUnsubscribeAttribute(string template)
|
||||||
|
: base(_supportedMethods, template)
|
||||||
|
=> ArgumentNullException.ThrowIfNull(template);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to mark a parameter as obsolete.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Parameter)]
|
||||||
|
public sealed class ParameterObsoleteAttribute : Attribute
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Attribute to mark a parameter as obsolete.
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Parameter)]
|
|
||||||
public sealed class ParameterObsoleteAttribute : Attribute
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "audio/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesAudioFileAttribute()
|
public sealed class ProducesAudioFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
private const string ContentType = "audio/*";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProducesAudioFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ProducesAudioFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,28 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal produces image attribute.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Method)]
|
||||||
|
public class ProducesFileAttribute : Attribute
|
||||||
{
|
{
|
||||||
|
private readonly string[] _contentTypes;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal produces image attribute.
|
/// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[AttributeUsage(AttributeTargets.Method)]
|
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
||||||
public class ProducesFileAttribute : Attribute
|
public ProducesFileAttribute(params string[] contentTypes)
|
||||||
{
|
{
|
||||||
private readonly string[] _contentTypes;
|
_contentTypes = contentTypes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ProducesFileAttribute"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="contentTypes">Content types this endpoint produces.</param>
|
|
||||||
public ProducesFileAttribute(params string[] contentTypes)
|
|
||||||
{
|
|
||||||
_contentTypes = contentTypes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the configured content types.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>the configured content types.</returns>
|
|
||||||
public string[] ContentTypes => _contentTypes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the configured content types.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>the configured content types.</returns>
|
||||||
|
public string[] ContentTypes => _contentTypes;
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesImageFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "image/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesImageFileAttribute()
|
public sealed class ProducesImageFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
private const string ContentType = "image/*";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProducesImageFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ProducesImageFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "image/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "application/x-mpegURL";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
|
/// Produces file attribute of "image/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesPlaylistFileAttribute()
|
public sealed class ProducesPlaylistFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
private const string ContentType = "application/x-mpegURL";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProducesPlaylistFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ProducesPlaylistFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
namespace Jellyfin.Api.Attributes
|
namespace Jellyfin.Api.Attributes;
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Produces file attribute of "video/*".
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
|
|
||||||
{
|
|
||||||
private const string ContentType = "video/*";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
|
/// Produces file attribute of "video/*".
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ProducesVideoFileAttribute()
|
public sealed class ProducesVideoFileAttribute : ProducesFileAttribute
|
||||||
: base(ContentType)
|
{
|
||||||
{
|
private const string ContentType = "video/*";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProducesVideoFileAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public ProducesVideoFileAttribute()
|
||||||
|
: base(ContentType)
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
@ -29,7 +30,7 @@ namespace Jellyfin.Api.Auth.AnonymousLanAccessPolicy
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AnonymousLanAccessRequirement requirement)
|
||||||
{
|
{
|
||||||
var ip = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
|
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
|
||||||
|
|
||||||
// Loopback will be on LAN, so we can accept null.
|
// Loopback will be on LAN, so we can accept null.
|
||||||
if (ip is null || _networkManager.IsInLocalNetwork(ip))
|
if (ip is null || _networkManager.IsInLocalNetwork(ip))
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
using System.Security.Claims;
|
|
||||||
using Jellyfin.Api.Extensions;
|
|
||||||
using Jellyfin.Api.Helpers;
|
|
||||||
using Jellyfin.Data.Enums;
|
|
||||||
using MediaBrowser.Common.Extensions;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base authorization handler.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
|
|
||||||
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
|
|
||||||
where T : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
private readonly IUserManager _userManager;
|
|
||||||
private readonly INetworkManager _networkManager;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
protected BaseAuthorizationHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_networkManager = networkManager;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate authenticated claims.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="claimsPrincipal">Request claims.</param>
|
|
||||||
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
|
|
||||||
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
|
|
||||||
/// <param name="requiredDownloadPermission">Whether validation requires download permission.</param>
|
|
||||||
/// <returns>Validated claim status.</returns>
|
|
||||||
protected bool ValidateClaims(
|
|
||||||
ClaimsPrincipal claimsPrincipal,
|
|
||||||
bool ignoreSchedule = false,
|
|
||||||
bool localAccessOnly = false,
|
|
||||||
bool requiredDownloadPermission = false)
|
|
||||||
{
|
|
||||||
// ApiKey is currently global admin, always allow.
|
|
||||||
var isApiKey = claimsPrincipal.GetIsApiKey();
|
|
||||||
if (isApiKey)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure claim has userId.
|
|
||||||
var userId = claimsPrincipal.GetUserId();
|
|
||||||
if (userId.Equals(default))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure userId links to a valid user.
|
|
||||||
var user = _userManager.GetUserById(userId);
|
|
||||||
if (user is null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure user is not disabled.
|
|
||||||
if (user.HasPermission(PermissionKind.IsDisabled))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
|
|
||||||
&& _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
|
|
||||||
|
|
||||||
// User cannot access remotely and user is remote
|
|
||||||
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localAccessOnly && !isInLocalNetwork)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User attempting to access out of parental control hours.
|
|
||||||
if (!ignoreSchedule
|
|
||||||
&& !user.HasPermission(PermissionKind.IsAdministrator)
|
|
||||||
&& !user.IsParentalScheduleAllowed())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User attempting to download without permission.
|
|
||||||
if (requiredDownloadPermission
|
|
||||||
&& !user.HasPermission(PermissionKind.EnableContentDownloading))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,8 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Api.Extensions;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -9,8 +13,12 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default authorization handler.
|
/// Default authorization handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
|
public class DefaultAuthorizationHandler : AuthorizationHandler<DefaultAuthorizationRequirement>
|
||||||
{
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
|
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -21,21 +29,56 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
|||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor)
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User);
|
var isApiKey = context.User.GetIsApiKey();
|
||||||
if (validated)
|
var userId = context.User.GetUserId();
|
||||||
|
// This likely only happens during the wizard, so skip the default checks and let any other handlers do it
|
||||||
|
if (!isApiKey && userId.Equals(default))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var isInLocalNetwork = _httpContextAccessor.HttpContext is not null
|
||||||
|
&& _networkManager.IsInLocalNetwork(_httpContextAccessor.HttpContext.GetNormalizedRemoteIp());
|
||||||
|
var user = _userManager.GetUserById(userId);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// User cannot access remotely and user is remote
|
||||||
|
if (!isInLocalNetwork && !user.HasPermission(PermissionKind.EnableRemoteAccess))
|
||||||
{
|
{
|
||||||
context.Fail();
|
context.Fail();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admins can do everything
|
||||||
|
if (isApiKey || context.User.IsInRole(UserRoles.Administrator))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's not great to have this check, but parental schedule must usually be honored except in a few rare cases
|
||||||
|
if (requirement.ValidateParentalSchedule && !user.IsParentalScheduleAllowed())
|
||||||
|
{
|
||||||
|
context.Fail();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only succeed if the requirement isn't a subclass as any subclassed requirement will handle success in its own handler
|
||||||
|
if (requirement.GetType() == typeof(DefaultAuthorizationRequirement))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -7,5 +7,18 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="DefaultAuthorizationRequirement"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="validateParentalSchedule">A value indicating whether to validate parental schedule.</param>
|
||||||
|
public DefaultAuthorizationRequirement(bool validateParentalSchedule = true)
|
||||||
|
{
|
||||||
|
ValidateParentalSchedule = validateParentalSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether to ignore parental schedule.
|
||||||
|
/// </summary>
|
||||||
|
public bool ValidateParentalSchedule { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Download authorization handler.
|
|
||||||
/// </summary>
|
|
||||||
public class DownloadHandler : BaseAuthorizationHandler<DownloadRequirement>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="DownloadHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public DownloadHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DownloadRequirement requirement)
|
|
||||||
{
|
|
||||||
var validated = ValidateClaims(context.User);
|
|
||||||
if (validated)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.DownloadPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The download permission requirement.
|
|
||||||
/// </summary>
|
|
||||||
public class DownloadRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ignore parental control schedule and allow before startup wizard has been completed.
|
|
||||||
/// </summary>
|
|
||||||
public class FirstTimeOrIgnoreParentalControlSetupHandler : BaseAuthorizationHandler<FirstTimeOrIgnoreParentalControlSetupRequirement>
|
|
||||||
{
|
|
||||||
private readonly IConfigurationManager _configurationManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="FirstTimeOrIgnoreParentalControlSetupHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
|
||||||
public FirstTimeOrIgnoreParentalControlSetupHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
IConfigurationManager configurationManager)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
_configurationManager = configurationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeOrIgnoreParentalControlSetupRequirement requirement)
|
|
||||||
{
|
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
|
||||||
if (validated)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeOrIgnoreParentalControlSetupPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// First time setup or ignore parental controls requirement.
|
|
||||||
/// </summary>
|
|
||||||
public class FirstTimeOrIgnoreParentalControlSetupRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Authorization handler for requiring first time setup or default privileges.
|
|
||||||
/// </summary>
|
|
||||||
public class FirstTimeSetupOrDefaultHandler : BaseAuthorizationHandler<FirstTimeSetupOrDefaultRequirement>
|
|
||||||
{
|
|
||||||
private readonly IConfigurationManager _configurationManager;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrDefaultHandler" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public FirstTimeSetupOrDefaultHandler(
|
|
||||||
IConfigurationManager configurationManager,
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
_configurationManager = configurationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrDefaultRequirement requirement)
|
|
||||||
{
|
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var validated = ValidateClaims(context.User);
|
|
||||||
if (validated)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrDefaultPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
|
|
||||||
/// </summary>
|
|
||||||
public class FirstTimeSetupOrDefaultRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The authorization requirement, requiring incomplete first time setup or elevated privileges, for the authorization handler.
|
|
||||||
/// </summary>
|
|
||||||
public class FirstTimeSetupOrElevatedRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +1,36 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using Jellyfin.Api.Extensions;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authorization handler for requiring first time setup or elevated privileges.
|
/// Authorization handler for requiring first time setup or default privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
|
public class FirstTimeSetupHandler : AuthorizationHandler<FirstTimeSetupRequirement>
|
||||||
{
|
{
|
||||||
private readonly IConfigurationManager _configurationManager;
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
|
/// Initializes a new instance of the <see cref="FirstTimeSetupHandler" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
public FirstTimeSetupHandler(
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public FirstTimeSetupOrElevatedHandler(
|
|
||||||
IConfigurationManager configurationManager,
|
IConfigurationManager configurationManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager)
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
{
|
||||||
_configurationManager = configurationManager;
|
_configurationManager = configurationManager;
|
||||||
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupOrElevatedRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FirstTimeSetupRequirement requirement)
|
||||||
{
|
{
|
||||||
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
|
||||||
{
|
{
|
||||||
@ -41,14 +38,27 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validated = ValidateClaims(context.User);
|
if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
|
||||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
context.Fail();
|
context.Fail();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requirement.ValidateParentalSchedule)
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = _userManager.GetUserById(context.User.GetUserId());
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.IsParentalScheduleAllowed())
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
@ -0,0 +1,25 @@
|
|||||||
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The authorization requirement, requiring incomplete first time setup or default privileges, for the authorization handler.
|
||||||
|
/// </summary>
|
||||||
|
public class FirstTimeSetupRequirement : DefaultAuthorizationRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="FirstTimeSetupRequirement"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="validateParentalSchedule">A value indicating whether to ignore parental schedule.</param>
|
||||||
|
/// <param name="requireAdmin">A value indicating whether administrator role is required.</param>
|
||||||
|
public FirstTimeSetupRequirement(bool validateParentalSchedule = false, bool requireAdmin = true) : base(validateParentalSchedule)
|
||||||
|
{
|
||||||
|
RequireAdmin = requireAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether administrator role is required.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireAdmin { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,44 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Escape schedule controls handler.
|
|
||||||
/// </summary>
|
|
||||||
public class IgnoreParentalControlHandler : BaseAuthorizationHandler<IgnoreParentalControlRequirement>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="IgnoreParentalControlHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public IgnoreParentalControlHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreParentalControlRequirement requirement)
|
|
||||||
{
|
|
||||||
var validated = ValidateClaims(context.User, ignoreSchedule: true);
|
|
||||||
if (validated)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.IgnoreParentalControlPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Escape schedule controls requirement.
|
|
||||||
/// </summary>
|
|
||||||
public class IgnoreParentalControlRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Net;
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
@ -10,27 +10,38 @@ namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Local access or require elevated privileges handler.
|
/// Local access or require elevated privileges handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class LocalAccessOrRequiresElevationHandler : BaseAuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
|
public class LocalAccessOrRequiresElevationHandler : AuthorizationHandler<LocalAccessOrRequiresElevationRequirement>
|
||||||
{
|
{
|
||||||
|
private readonly INetworkManager _networkManager;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
|
/// Initializes a new instance of the <see cref="LocalAccessOrRequiresElevationHandler"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||||
public LocalAccessOrRequiresElevationHandler(
|
public LocalAccessOrRequiresElevationHandler(
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor)
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
{
|
||||||
|
_networkManager = networkManager;
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessOrRequiresElevationRequirement requirement)
|
||||||
{
|
{
|
||||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
var ip = _httpContextAccessor.HttpContext?.GetNormalizedRemoteIp();
|
||||||
if (validated || context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
|
// Loopback will be on LAN, so we can accept null.
|
||||||
|
if (ip is null || _networkManager.IsInLocalNetwork(ip))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.User.IsInRole(UserRoles.Administrator))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
namespace Jellyfin.Api.Auth.LocalAccessOrRequiresElevationPolicy
|
||||||
{
|
{
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Local access handler.
|
|
||||||
/// </summary>
|
|
||||||
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public LocalAccessHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
|
|
||||||
{
|
|
||||||
var validated = ValidateClaims(context.User, localAccessOnly: true);
|
|
||||||
if (validated)
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.LocalAccessPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The local access authorization requirement.
|
|
||||||
/// </summary>
|
|
||||||
public class LocalAccessRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
using System.Threading.Tasks;
|
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using MediaBrowser.Common.Net;
|
|
||||||
using MediaBrowser.Controller.Library;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Authorization handler for requiring elevated privileges.
|
|
||||||
/// </summary>
|
|
||||||
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public RequiresElevationHandler(
|
|
||||||
IUserManager userManager,
|
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
|
|
||||||
{
|
|
||||||
var validated = ValidateClaims(context.User);
|
|
||||||
if (validated && context.User.IsInRole(UserRoles.Administrator))
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The authorization requirement for requiring elevated privileges in the authorization handler.
|
|
||||||
/// </summary>
|
|
||||||
public class RequiresElevationRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +1,17 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Net;
|
using MediaBrowser.Common.Extensions;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.SyncPlay;
|
using MediaBrowser.Controller.SyncPlay;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default authorization handler.
|
/// Default authorization handler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncPlayAccessHandler : BaseAuthorizationHandler<SyncPlayAccessRequirement>
|
public class SyncPlayAccessHandler : AuthorizationHandler<SyncPlayAccessRequirement>
|
||||||
{
|
{
|
||||||
private readonly ISyncPlayManager _syncPlayManager;
|
private readonly ISyncPlayManager _syncPlayManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
@ -23,14 +21,9 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
|
/// <param name="syncPlayManager">Instance of the <see cref="ISyncPlayManager"/> interface.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
|
|
||||||
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
|
||||||
public SyncPlayAccessHandler(
|
public SyncPlayAccessHandler(
|
||||||
ISyncPlayManager syncPlayManager,
|
ISyncPlayManager syncPlayManager,
|
||||||
IUserManager userManager,
|
IUserManager userManager)
|
||||||
INetworkManager networkManager,
|
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
: base(userManager, networkManager, httpContextAccessor)
|
|
||||||
{
|
{
|
||||||
_syncPlayManager = syncPlayManager;
|
_syncPlayManager = syncPlayManager;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
|
||||||
{
|
{
|
||||||
if (!ValidateClaims(context.User))
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userId = context.User.GetUserId();
|
var userId = context.User.GetUserId();
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
|
||||||
{
|
{
|
||||||
if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
|
if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups
|
||||||
|| user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
|
|
||||||
|| _syncPlayManager.IsUserActive(userId))
|
|| _syncPlayManager.IsUserActive(userId))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
|
||||||
{
|
{
|
||||||
@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
|
||||||
{
|
{
|
||||||
@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
|
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
|
||||||
{
|
{
|
||||||
@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
|||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.Fail();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default authorization requirement.
|
/// The default authorization requirement.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SyncPlayAccessRequirement : IAuthorizationRequirement
|
public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
/// Initializes a new instance of the <see cref="SyncPlayAccessRequirement"/> class.
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Api.Extensions;
|
||||||
|
using MediaBrowser.Common.Extensions;
|
||||||
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.UserPermissionPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// User permission authorization handler.
|
||||||
|
/// </summary>
|
||||||
|
public class UserPermissionHandler : AuthorizationHandler<UserPermissionRequirement>
|
||||||
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UserPermissionHandler"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
public UserPermissionHandler(IUserManager userManager)
|
||||||
|
{
|
||||||
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
|
||||||
|
{
|
||||||
|
var user = _userManager.GetUserById(context.User.GetUserId());
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
throw new ResourceNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.HasPermission(requirement.RequiredPermission))
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
|
||||||
|
namespace Jellyfin.Api.Auth.UserPermissionPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user permission requirement.
|
||||||
|
/// </summary>
|
||||||
|
public class UserPermissionRequirement : DefaultAuthorizationRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="UserPermissionRequirement"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="requiredPermission">The required <see cref="PermissionKind"/>.</param>
|
||||||
|
/// <param name="validateParentalSchedule">Whether to validate the user's parental schedule.</param>
|
||||||
|
public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule)
|
||||||
|
{
|
||||||
|
RequiredPermission = requiredPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the required user permission.
|
||||||
|
/// </summary>
|
||||||
|
public PermissionKind RequiredPermission { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -4,35 +4,34 @@ using Jellyfin.Api.Results;
|
|||||||
using Jellyfin.Extensions.Json;
|
using Jellyfin.Extensions.Json;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api
|
namespace Jellyfin.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base api controller for the API setting a default route.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("[controller]")]
|
||||||
|
[Produces(
|
||||||
|
MediaTypeNames.Application.Json,
|
||||||
|
JsonDefaults.CamelCaseMediaType,
|
||||||
|
JsonDefaults.PascalCaseMediaType)]
|
||||||
|
public class BaseJellyfinApiController : ControllerBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base api controller for the API setting a default route.
|
/// Create a new <see cref="OkResult{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
/// <param name="value">The value to return.</param>
|
||||||
[Route("[controller]")]
|
/// <typeparam name="T">The type to return.</typeparam>
|
||||||
[Produces(
|
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
||||||
MediaTypeNames.Application.Json,
|
protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
|
||||||
JsonDefaults.CamelCaseMediaType,
|
=> new OkResult<IEnumerable<T>?>(value);
|
||||||
JsonDefaults.PascalCaseMediaType)]
|
|
||||||
public class BaseJellyfinApiController : ControllerBase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new <see cref="OkResult{T}"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to return.</param>
|
|
||||||
/// <typeparam name="T">The type to return.</typeparam>
|
|
||||||
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
|
||||||
protected ActionResult<IEnumerable<T>> Ok<T>(IEnumerable<T>? value)
|
|
||||||
=> new OkResult<IEnumerable<T>?>(value);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="OkResult{T}"/>.
|
/// Create a new <see cref="OkResult{T}"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">The value to return.</param>
|
/// <param name="value">The value to return.</param>
|
||||||
/// <typeparam name="T">The type to return.</typeparam>
|
/// <typeparam name="T">The type to return.</typeparam>
|
||||||
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
/// <returns>The <see cref="ActionResult{T}"/>.</returns>
|
||||||
protected ActionResult<T> Ok<T>(T value)
|
protected ActionResult<T> Ok<T>(T value)
|
||||||
=> new OkResult<T>(value);
|
=> new OkResult<T>(value);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
namespace Jellyfin.Api.Constants
|
namespace Jellyfin.Api.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication schemes for user authentication in the API.
|
||||||
|
/// </summary>
|
||||||
|
public static class AuthenticationSchemes
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication schemes for user authentication in the API.
|
/// Scheme name for the custom legacy authentication.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AuthenticationSchemes
|
public const string CustomAuthentication = "CustomAuthentication";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Scheme name for the custom legacy authentication.
|
|
||||||
/// </summary>
|
|
||||||
public const string CustomAuthentication = "CustomAuthentication";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,43 +1,42 @@
|
|||||||
namespace Jellyfin.Api.Constants
|
namespace Jellyfin.Api.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal claim types for authorization.
|
||||||
|
/// </summary>
|
||||||
|
public static class InternalClaimTypes
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal claim types for authorization.
|
/// User Id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InternalClaimTypes
|
public const string UserId = "Jellyfin-UserId";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// User Id.
|
|
||||||
/// </summary>
|
|
||||||
public const string UserId = "Jellyfin-UserId";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Device Id.
|
/// Device Id.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string DeviceId = "Jellyfin-DeviceId";
|
public const string DeviceId = "Jellyfin-DeviceId";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Device.
|
/// Device.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Device = "Jellyfin-Device";
|
public const string Device = "Jellyfin-Device";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client.
|
/// Client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Client = "Jellyfin-Client";
|
public const string Client = "Jellyfin-Client";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version.
|
/// Version.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Version = "Jellyfin-Version";
|
public const string Version = "Jellyfin-Version";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Token.
|
/// Token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Token = "Jellyfin-Token";
|
public const string Token = "Jellyfin-Token";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is Api Key.
|
/// Is Api Key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string IsApiKey = "Jellyfin-IsApiKey";
|
public const string IsApiKey = "Jellyfin-IsApiKey";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,78 +1,87 @@
|
|||||||
namespace Jellyfin.Api.Constants
|
namespace Jellyfin.Api.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policies for the API authorization.
|
||||||
|
/// </summary>
|
||||||
|
public static class Policies
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policies for the API authorization.
|
/// Policy name for requiring first time setup or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Policies
|
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Policy name for default authorization.
|
|
||||||
/// </summary>
|
|
||||||
public const string DefaultAuthorization = "DefaultAuthorization";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or elevated privileges.
|
/// Policy name for requiring elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
|
public const string RequiresElevation = "RequiresElevation";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring elevated privileges.
|
/// Policy name for allowing local access only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RequiresElevation = "RequiresElevation";
|
public const string LocalAccessOnly = "LocalAccessOnly";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for allowing local access only.
|
/// Policy name for escaping schedule controls.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LocalAccessOnly = "LocalAccessOnly";
|
public const string IgnoreParentalControl = "IgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for escaping schedule controls.
|
/// Policy name for requiring download permission.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string IgnoreParentalControl = "IgnoreParentalControl";
|
public const string Download = "Download";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring download permission.
|
/// Policy name for requiring first time setup or default permissions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Download = "Download";
|
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring first time setup or default permissions.
|
/// Policy name for requiring local access or elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
|
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring local access or elevated privileges.
|
/// Policy name for requiring (anonymous) LAN access.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
|
public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for requiring (anonymous) LAN access.
|
/// Policy name for escaping schedule controls or requiring first time setup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
|
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for escaping schedule controls or requiring first time setup.
|
/// Policy name for accessing SyncPlay.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
|
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for accessing SyncPlay.
|
/// Policy name for creating a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
|
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for creating a SyncPlay group.
|
/// Policy name for joining a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
|
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for joining a SyncPlay group.
|
/// Policy name for accessing a SyncPlay group.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
|
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Policy name for accessing a SyncPlay group.
|
/// Policy name for accessing collection management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
|
public const string CollectionManagement = "CollectionManagement";
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for accessing LiveTV.
|
||||||
|
/// </summary>
|
||||||
|
public const string LiveTvAccess = "LiveTvAccess";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy name for managing LiveTV.
|
||||||
|
/// </summary>
|
||||||
|
public const string LiveTvManagement = "LiveTvManagement";
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
namespace Jellyfin.Api.Constants
|
namespace Jellyfin.Api.Constants;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constants for user roles used in the authentication and authorization for the API.
|
||||||
|
/// </summary>
|
||||||
|
public static class UserRoles
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constants for user roles used in the authentication and authorization for the API.
|
/// Guest user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UserRoles
|
public const string Guest = "Guest";
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Guest user.
|
|
||||||
/// </summary>
|
|
||||||
public const string Guest = "Guest";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Regular user with no special privileges.
|
/// Regular user with no special privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string User = "User";
|
public const string User = "User";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Administrator user with elevated privileges.
|
/// Administrator user with elevated privileges.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Administrator = "Administrator";
|
public const string Administrator = "Administrator";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activity log controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("System/ActivityLog")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public class ActivityLogController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IActivityManager _activityManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Activity log controller.
|
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System/ActivityLog")]
|
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
public ActivityLogController(IActivityManager activityManager)
|
||||||
public class ActivityLogController : BaseJellyfinApiController
|
|
||||||
{
|
{
|
||||||
private readonly IActivityManager _activityManager;
|
_activityManager = activityManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ActivityLogController"/> class.
|
/// Gets activity log entries.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="activityManager">Instance of <see cref="IActivityManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
public ActivityLogController(IActivityManager activityManager)
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
|
||||||
|
/// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
|
||||||
|
/// <response code="200">Activity log returned.</response>
|
||||||
|
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
||||||
|
[HttpGet("Entries")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery] DateTime? minDate,
|
||||||
|
[FromQuery] bool? hasUserId)
|
||||||
|
{
|
||||||
|
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
|
||||||
{
|
{
|
||||||
_activityManager = activityManager;
|
Skip = startIndex,
|
||||||
}
|
Limit = limit,
|
||||||
|
MinDate = minDate,
|
||||||
/// <summary>
|
HasUserId = hasUserId
|
||||||
/// Gets activity log entries.
|
}).ConfigureAwait(false);
|
||||||
/// </summary>
|
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
|
||||||
/// <param name="minDate">Optional. The minimum date. Format = ISO.</param>
|
|
||||||
/// <param name="hasUserId">Optional. Filter log entries if it has user id, or not.</param>
|
|
||||||
/// <response code="200">Activity log returned.</response>
|
|
||||||
/// <returns>A <see cref="QueryResult{ActivityLogEntry}"/> containing the log entries.</returns>
|
|
||||||
[HttpGet("Entries")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<ActionResult<QueryResult<ActivityLogEntry>>> GetLogEntries(
|
|
||||||
[FromQuery] int? startIndex,
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery] DateTime? minDate,
|
|
||||||
[FromQuery] bool? hasUserId)
|
|
||||||
{
|
|
||||||
return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
|
|
||||||
{
|
|
||||||
Skip = startIndex,
|
|
||||||
Limit = limit,
|
|
||||||
MinDate = minDate,
|
|
||||||
HasUserId = hasUserId
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Auth")]
|
||||||
|
public class ApiKeyController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IAuthenticationManager _authenticationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication controller.
|
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Auth")]
|
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
|
||||||
public class ApiKeyController : BaseJellyfinApiController
|
public ApiKeyController(IAuthenticationManager authenticationManager)
|
||||||
{
|
{
|
||||||
private readonly IAuthenticationManager _authenticationManager;
|
_authenticationManager = authenticationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ApiKeyController"/> class.
|
/// Get all keys.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="authenticationManager">Instance of <see cref="IAuthenticationManager"/> interface.</param>
|
/// <response code="200">Api keys retrieved.</response>
|
||||||
public ApiKeyController(IAuthenticationManager authenticationManager)
|
/// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
|
||||||
{
|
[HttpGet("Keys")]
|
||||||
_authenticationManager = authenticationManager;
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
}
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
|
||||||
|
{
|
||||||
|
var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
return new QueryResult<AuthenticationInfo>(keys);
|
||||||
/// Get all keys.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Api keys retrieved.</response>
|
|
||||||
/// <returns>A <see cref="QueryResult{AuthenticationInfo}"/> with all keys.</returns>
|
|
||||||
[HttpGet("Keys")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<ActionResult<QueryResult<AuthenticationInfo>>> GetKeys()
|
|
||||||
{
|
|
||||||
var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new QueryResult<AuthenticationInfo>(keys);
|
/// <summary>
|
||||||
}
|
/// Create a new api key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="app">Name of the app using the authentication key.</param>
|
||||||
|
/// <response code="204">Api key created.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpPost("Keys")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
|
||||||
|
{
|
||||||
|
await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// Create a new api key.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="app">Name of the app using the authentication key.</param>
|
|
||||||
/// <response code="204">Api key created.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
|
||||||
[HttpPost("Keys")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> CreateKey([FromQuery, Required] string app)
|
|
||||||
{
|
|
||||||
await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return NoContent();
|
/// <summary>
|
||||||
}
|
/// Remove an api key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The access token to delete.</param>
|
||||||
|
/// <response code="204">Api key deleted.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpDelete("Keys/{key}")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
|
||||||
|
{
|
||||||
|
await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// Remove an api key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The access token to delete.</param>
|
|
||||||
/// <response code="204">Api key deleted.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
|
||||||
[HttpDelete("Keys/{key}")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> RevokeKey([FromRoute, Required] string key)
|
|
||||||
{
|
|
||||||
await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
@ -17,464 +16,463 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The artists controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Artists")]
|
||||||
|
[Authorize]
|
||||||
|
public class ArtistsController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IDtoService _dtoService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The artists controller.
|
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Artists")]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class ArtistsController : BaseJellyfinApiController
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
|
public ArtistsController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IUserManager userManager,
|
||||||
|
IDtoService dtoService)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ArtistsController"/> class.
|
/// Gets all artists from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
public ArtistsController(
|
/// <param name="searchTerm">Optional. Search term.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
IUserManager userManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
IDtoService dtoService)
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
|
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||||
|
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
|
||||||
|
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||||
|
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
||||||
|
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||||
|
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
|
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||||
|
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||||
|
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||||
|
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||||
|
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||||
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
|
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||||
|
/// <response code="200">Artists returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
|
||||||
|
[FromQuery] double? minCommunityRating,
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery] string? searchTerm,
|
||||||
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
|
[FromQuery] bool? isFavorite,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||||
|
[FromQuery] bool? enableUserData,
|
||||||
|
[FromQuery] int? imageTypeLimit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
|
[FromQuery] string? person,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] string? nameStartsWithOrGreater,
|
||||||
|
[FromQuery] string? nameStartsWith,
|
||||||
|
[FromQuery] string? nameLessThan,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
|
[FromQuery] bool? enableImages = true,
|
||||||
|
[FromQuery] bool enableTotalRecordCount = true)
|
||||||
|
{
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
|
User? user = null;
|
||||||
|
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||||
|
|
||||||
|
if (userId.HasValue && !userId.Equals(default))
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
user = _userManager.GetUserById(userId.Value);
|
||||||
_userManager = userManager;
|
|
||||||
_dtoService = dtoService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var query = new InternalItemsQuery(user)
|
||||||
/// Gets all artists from a given item, folder, or the entire library.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
|
||||||
/// <param name="searchTerm">Optional. Search term.</param>
|
|
||||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
|
||||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
|
||||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
|
||||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
|
|
||||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
|
||||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
|
||||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
|
||||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="userId">User id.</param>
|
|
||||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
|
||||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
|
||||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
|
||||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
|
||||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
|
||||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
|
||||||
/// <response code="200">Artists returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the artists.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetArtists(
|
|
||||||
[FromQuery] double? minCommunityRating,
|
|
||||||
[FromQuery] int? startIndex,
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery] string? searchTerm,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
|
||||||
[FromQuery] bool? isFavorite,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
|
||||||
[FromQuery] bool? enableUserData,
|
|
||||||
[FromQuery] int? imageTypeLimit,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
|
||||||
[FromQuery] string? person,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] string? nameStartsWithOrGreater,
|
|
||||||
[FromQuery] string? nameStartsWith,
|
|
||||||
[FromQuery] string? nameLessThan,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
|
||||||
[FromQuery] bool? enableImages = true,
|
|
||||||
[FromQuery] bool enableTotalRecordCount = true)
|
|
||||||
{
|
{
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
.AddClientFields(User)
|
IncludeItemTypes = includeItemTypes,
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
MediaTypes = mediaTypes,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
Tags = tags,
|
||||||
|
OfficialRatings = officialRatings,
|
||||||
|
Genres = genres,
|
||||||
|
GenreIds = genreIds,
|
||||||
|
StudioIds = studioIds,
|
||||||
|
Person = person,
|
||||||
|
PersonIds = personIds,
|
||||||
|
PersonTypes = personTypes,
|
||||||
|
Years = years,
|
||||||
|
MinCommunityRating = minCommunityRating,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||||
|
};
|
||||||
|
|
||||||
User? user = null;
|
if (parentId.HasValue)
|
||||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
{
|
||||||
|
if (parentItem is Folder)
|
||||||
if (userId.HasValue && !userId.Equals(default))
|
|
||||||
{
|
{
|
||||||
user = _userManager.GetUserById(userId.Value);
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
{
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
query.ItemIds = new[] { parentId.Value };
|
||||||
IncludeItemTypes = includeItemTypes,
|
|
||||||
MediaTypes = mediaTypes,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
Limit = limit,
|
|
||||||
IsFavorite = isFavorite,
|
|
||||||
NameLessThan = nameLessThan,
|
|
||||||
NameStartsWith = nameStartsWith,
|
|
||||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
|
||||||
Tags = tags,
|
|
||||||
OfficialRatings = officialRatings,
|
|
||||||
Genres = genres,
|
|
||||||
GenreIds = genreIds,
|
|
||||||
StudioIds = studioIds,
|
|
||||||
Person = person,
|
|
||||||
PersonIds = personIds,
|
|
||||||
PersonTypes = personTypes,
|
|
||||||
Years = years,
|
|
||||||
MinCommunityRating = minCommunityRating,
|
|
||||||
DtoOptions = dtoOptions,
|
|
||||||
SearchTerm = searchTerm,
|
|
||||||
EnableTotalRecordCount = enableTotalRecordCount,
|
|
||||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parentId.HasValue)
|
|
||||||
{
|
|
||||||
if (parentItem is Folder)
|
|
||||||
{
|
|
||||||
query.AncestorIds = new[] { parentId.Value };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
query.ItemIds = new[] { parentId.Value };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Studios
|
|
||||||
if (studios.Length != 0)
|
|
||||||
{
|
|
||||||
query.StudioIds = studios.Select(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _libraryManager.GetStudio(i);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var filter in filters)
|
|
||||||
{
|
|
||||||
switch (filter)
|
|
||||||
{
|
|
||||||
case ItemFilter.Dislikes:
|
|
||||||
query.IsLiked = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavorite:
|
|
||||||
query.IsFavorite = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavoriteOrLikes:
|
|
||||||
query.IsFavoriteOrLiked = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFolder:
|
|
||||||
query.IsFolder = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsNotFolder:
|
|
||||||
query.IsFolder = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsPlayed:
|
|
||||||
query.IsPlayed = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsResumable:
|
|
||||||
query.IsResumable = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsUnplayed:
|
|
||||||
query.IsPlayed = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.Likes:
|
|
||||||
query.IsLiked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = _libraryManager.GetArtists(query);
|
|
||||||
|
|
||||||
var dtos = result.Items.Select(i =>
|
|
||||||
{
|
|
||||||
var (baseItem, itemCounts) = i;
|
|
||||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
|
||||||
|
|
||||||
if (includeItemTypes.Length != 0)
|
|
||||||
{
|
|
||||||
dto.ChildCount = itemCounts.ItemCount;
|
|
||||||
dto.ProgramCount = itemCounts.ProgramCount;
|
|
||||||
dto.SeriesCount = itemCounts.SeriesCount;
|
|
||||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
|
||||||
dto.MovieCount = itemCounts.MovieCount;
|
|
||||||
dto.TrailerCount = itemCounts.TrailerCount;
|
|
||||||
dto.AlbumCount = itemCounts.AlbumCount;
|
|
||||||
dto.SongCount = itemCounts.SongCount;
|
|
||||||
dto.ArtistCount = itemCounts.ArtistCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new QueryResult<BaseItemDto>(
|
|
||||||
query.StartIndex,
|
|
||||||
result.TotalRecordCount,
|
|
||||||
dtos.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// Studios
|
||||||
/// Gets all album artists from a given item, folder, or the entire library.
|
if (studios.Length != 0)
|
||||||
/// </summary>
|
|
||||||
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
|
||||||
/// <param name="searchTerm">Optional. Search term.</param>
|
|
||||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
|
||||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
|
||||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
|
||||||
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
|
|
||||||
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
|
||||||
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
|
||||||
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
|
||||||
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
|
||||||
/// <param name="userId">User id.</param>
|
|
||||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
|
||||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
|
||||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
|
||||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
|
||||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
|
||||||
/// <param name="enableTotalRecordCount">Total record count.</param>
|
|
||||||
/// <response code="200">Album artists returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
|
|
||||||
[HttpGet("AlbumArtists")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
|
|
||||||
[FromQuery] double? minCommunityRating,
|
|
||||||
[FromQuery] int? startIndex,
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery] string? searchTerm,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
|
||||||
[FromQuery] bool? isFavorite,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
|
||||||
[FromQuery] bool? enableUserData,
|
|
||||||
[FromQuery] int? imageTypeLimit,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
|
||||||
[FromQuery] string? person,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] string? nameStartsWithOrGreater,
|
|
||||||
[FromQuery] string? nameStartsWith,
|
|
||||||
[FromQuery] string? nameLessThan,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
|
||||||
[FromQuery] bool? enableImages = true,
|
|
||||||
[FromQuery] bool enableTotalRecordCount = true)
|
|
||||||
{
|
{
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
query.StudioIds = studios.Select(i =>
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
|
|
||||||
User? user = null;
|
|
||||||
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
|
||||||
|
|
||||||
if (userId.HasValue && !userId.Equals(default))
|
|
||||||
{
|
{
|
||||||
user = _userManager.GetUserById(userId.Value);
|
try
|
||||||
}
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
|
||||||
IncludeItemTypes = includeItemTypes,
|
|
||||||
MediaTypes = mediaTypes,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
Limit = limit,
|
|
||||||
IsFavorite = isFavorite,
|
|
||||||
NameLessThan = nameLessThan,
|
|
||||||
NameStartsWith = nameStartsWith,
|
|
||||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
|
||||||
Tags = tags,
|
|
||||||
OfficialRatings = officialRatings,
|
|
||||||
Genres = genres,
|
|
||||||
GenreIds = genreIds,
|
|
||||||
StudioIds = studioIds,
|
|
||||||
Person = person,
|
|
||||||
PersonIds = personIds,
|
|
||||||
PersonTypes = personTypes,
|
|
||||||
Years = years,
|
|
||||||
MinCommunityRating = minCommunityRating,
|
|
||||||
DtoOptions = dtoOptions,
|
|
||||||
SearchTerm = searchTerm,
|
|
||||||
EnableTotalRecordCount = enableTotalRecordCount,
|
|
||||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parentId.HasValue)
|
|
||||||
{
|
|
||||||
if (parentItem is Folder)
|
|
||||||
{
|
{
|
||||||
query.AncestorIds = new[] { parentId.Value };
|
return _libraryManager.GetStudio(i);
|
||||||
}
|
}
|
||||||
else
|
catch
|
||||||
{
|
{
|
||||||
query.ItemIds = new[] { parentId.Value };
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
||||||
|
|
||||||
// Studios
|
|
||||||
if (studios.Length != 0)
|
|
||||||
{
|
|
||||||
query.StudioIds = studios.Select(i =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _libraryManager.GetStudio(i);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var filter in filters)
|
|
||||||
{
|
|
||||||
switch (filter)
|
|
||||||
{
|
|
||||||
case ItemFilter.Dislikes:
|
|
||||||
query.IsLiked = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavorite:
|
|
||||||
query.IsFavorite = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavoriteOrLikes:
|
|
||||||
query.IsFavoriteOrLiked = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFolder:
|
|
||||||
query.IsFolder = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsNotFolder:
|
|
||||||
query.IsFolder = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsPlayed:
|
|
||||||
query.IsPlayed = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsResumable:
|
|
||||||
query.IsResumable = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsUnplayed:
|
|
||||||
query.IsPlayed = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.Likes:
|
|
||||||
query.IsLiked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = _libraryManager.GetAlbumArtists(query);
|
|
||||||
|
|
||||||
var dtos = result.Items.Select(i =>
|
|
||||||
{
|
|
||||||
var (baseItem, itemCounts) = i;
|
|
||||||
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
|
||||||
|
|
||||||
if (includeItemTypes.Length != 0)
|
|
||||||
{
|
|
||||||
dto.ChildCount = itemCounts.ItemCount;
|
|
||||||
dto.ProgramCount = itemCounts.ProgramCount;
|
|
||||||
dto.SeriesCount = itemCounts.SeriesCount;
|
|
||||||
dto.EpisodeCount = itemCounts.EpisodeCount;
|
|
||||||
dto.MovieCount = itemCounts.MovieCount;
|
|
||||||
dto.TrailerCount = itemCounts.TrailerCount;
|
|
||||||
dto.AlbumCount = itemCounts.AlbumCount;
|
|
||||||
dto.SongCount = itemCounts.SongCount;
|
|
||||||
dto.ArtistCount = itemCounts.ArtistCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
});
|
|
||||||
|
|
||||||
return new QueryResult<BaseItemDto>(
|
|
||||||
query.StartIndex,
|
|
||||||
result.TotalRecordCount,
|
|
||||||
dtos.ToArray());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
foreach (var filter in filters)
|
||||||
/// Gets an artist by name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">Studio name.</param>
|
|
||||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
|
||||||
/// <response code="200">Artist returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
|
|
||||||
[HttpGet("{name}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
|
||||||
{
|
{
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
switch (filter)
|
||||||
|
|
||||||
var item = _libraryManager.GetArtist(name, dtoOptions);
|
|
||||||
|
|
||||||
if (userId.HasValue && !userId.Value.Equals(default))
|
|
||||||
{
|
{
|
||||||
var user = _userManager.GetUserById(userId.Value);
|
case ItemFilter.Dislikes:
|
||||||
|
query.IsLiked = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFavorite:
|
||||||
|
query.IsFavorite = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
|
query.IsFavoriteOrLiked = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFolder:
|
||||||
|
query.IsFolder = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsNotFolder:
|
||||||
|
query.IsFolder = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsPlayed:
|
||||||
|
query.IsPlayed = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsResumable:
|
||||||
|
query.IsResumable = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsUnplayed:
|
||||||
|
query.IsPlayed = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.Likes:
|
||||||
|
query.IsLiked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
var result = _libraryManager.GetArtists(query);
|
||||||
|
|
||||||
|
var dtos = result.Items.Select(i =>
|
||||||
|
{
|
||||||
|
var (baseItem, itemCounts) = i;
|
||||||
|
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||||
|
|
||||||
|
if (includeItemTypes.Length != 0)
|
||||||
|
{
|
||||||
|
dto.ChildCount = itemCounts.ItemCount;
|
||||||
|
dto.ProgramCount = itemCounts.ProgramCount;
|
||||||
|
dto.SeriesCount = itemCounts.SeriesCount;
|
||||||
|
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||||
|
dto.MovieCount = itemCounts.MovieCount;
|
||||||
|
dto.TrailerCount = itemCounts.TrailerCount;
|
||||||
|
dto.AlbumCount = itemCounts.AlbumCount;
|
||||||
|
dto.SongCount = itemCounts.SongCount;
|
||||||
|
dto.ArtistCount = itemCounts.ArtistCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
return dto;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new QueryResult<BaseItemDto>(
|
||||||
|
query.StartIndex,
|
||||||
|
result.TotalRecordCount,
|
||||||
|
dtos.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all album artists from a given item, folder, or the entire library.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minCommunityRating">Optional filter by minimum community rating.</param>
|
||||||
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="searchTerm">Optional. Search term.</param>
|
||||||
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
|
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</param>
|
||||||
|
/// <param name="mediaTypes">Optional filter by MediaType. Allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="genres">Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="genreIds">Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="officialRatings">Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="tags">Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="years">Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.</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>
|
||||||
|
/// <param name="person">Optional. If specified, results will be filtered to include only those containing the specified person.</param>
|
||||||
|
/// <param name="personIds">Optional. If specified, results will be filtered to include only those containing the specified person ids.</param>
|
||||||
|
/// <param name="personTypes">Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.</param>
|
||||||
|
/// <param name="studios">Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="studioIds">Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.</param>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
|
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||||
|
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||||
|
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||||
|
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||||
|
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||||
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
|
/// <param name="enableTotalRecordCount">Total record count.</param>
|
||||||
|
/// <response code="200">Album artists returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the album artists.</returns>
|
||||||
|
[HttpGet("AlbumArtists")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetAlbumArtists(
|
||||||
|
[FromQuery] double? minCommunityRating,
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery] string? searchTerm,
|
||||||
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
|
[FromQuery] bool? isFavorite,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] genres,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] genreIds,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] officialRatings,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] tags,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] int[] years,
|
||||||
|
[FromQuery] bool? enableUserData,
|
||||||
|
[FromQuery] int? imageTypeLimit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
|
[FromQuery] string? person,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] personIds,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] personTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(PipeDelimitedArrayModelBinder))] string[] studios,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] studioIds,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] string? nameStartsWithOrGreater,
|
||||||
|
[FromQuery] string? nameStartsWith,
|
||||||
|
[FromQuery] string? nameLessThan,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
|
[FromQuery] bool? enableImages = true,
|
||||||
|
[FromQuery] bool enableTotalRecordCount = true)
|
||||||
|
{
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
|
User? user = null;
|
||||||
|
BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||||
|
|
||||||
|
if (userId.HasValue && !userId.Equals(default))
|
||||||
|
{
|
||||||
|
user = _userManager.GetUserById(userId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery(user)
|
||||||
|
{
|
||||||
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
|
IncludeItemTypes = includeItemTypes,
|
||||||
|
MediaTypes = mediaTypes,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
Tags = tags,
|
||||||
|
OfficialRatings = officialRatings,
|
||||||
|
Genres = genres,
|
||||||
|
GenreIds = genreIds,
|
||||||
|
StudioIds = studioIds,
|
||||||
|
Person = person,
|
||||||
|
PersonIds = personIds,
|
||||||
|
PersonTypes = personTypes,
|
||||||
|
Years = years,
|
||||||
|
MinCommunityRating = minCommunityRating,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parentId.HasValue)
|
||||||
|
{
|
||||||
|
if (parentItem is Folder)
|
||||||
|
{
|
||||||
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
query.ItemIds = new[] { parentId.Value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Studios
|
||||||
|
if (studios.Length != 0)
|
||||||
|
{
|
||||||
|
query.StudioIds = studios.Select(i =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _libraryManager.GetStudio(i);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).Where(i => i is not null).Select(i => i!.Id).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
switch (filter)
|
||||||
|
{
|
||||||
|
case ItemFilter.Dislikes:
|
||||||
|
query.IsLiked = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFavorite:
|
||||||
|
query.IsFavorite = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
|
query.IsFavoriteOrLiked = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsFolder:
|
||||||
|
query.IsFolder = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsNotFolder:
|
||||||
|
query.IsFolder = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsPlayed:
|
||||||
|
query.IsPlayed = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsResumable:
|
||||||
|
query.IsResumable = true;
|
||||||
|
break;
|
||||||
|
case ItemFilter.IsUnplayed:
|
||||||
|
query.IsPlayed = false;
|
||||||
|
break;
|
||||||
|
case ItemFilter.Likes:
|
||||||
|
query.IsLiked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = _libraryManager.GetAlbumArtists(query);
|
||||||
|
|
||||||
|
var dtos = result.Items.Select(i =>
|
||||||
|
{
|
||||||
|
var (baseItem, itemCounts) = i;
|
||||||
|
var dto = _dtoService.GetItemByNameDto(baseItem, dtoOptions, null, user);
|
||||||
|
|
||||||
|
if (includeItemTypes.Length != 0)
|
||||||
|
{
|
||||||
|
dto.ChildCount = itemCounts.ItemCount;
|
||||||
|
dto.ProgramCount = itemCounts.ProgramCount;
|
||||||
|
dto.SeriesCount = itemCounts.SeriesCount;
|
||||||
|
dto.EpisodeCount = itemCounts.EpisodeCount;
|
||||||
|
dto.MovieCount = itemCounts.MovieCount;
|
||||||
|
dto.TrailerCount = itemCounts.TrailerCount;
|
||||||
|
dto.AlbumCount = itemCounts.AlbumCount;
|
||||||
|
dto.SongCount = itemCounts.SongCount;
|
||||||
|
dto.ArtistCount = itemCounts.ArtistCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new QueryResult<BaseItemDto>(
|
||||||
|
query.StartIndex,
|
||||||
|
result.TotalRecordCount,
|
||||||
|
dtos.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an artist by name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Studio name.</param>
|
||||||
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
|
/// <response code="200">Artist returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the artist.</returns>
|
||||||
|
[HttpGet("{name}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<BaseItemDto> GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
|
||||||
|
{
|
||||||
|
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||||
|
|
||||||
|
var item = _libraryManager.GetArtist(name, dtoOptions);
|
||||||
|
|
||||||
|
if (userId.HasValue && !userId.Value.Equals(default))
|
||||||
|
{
|
||||||
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The audio controller.
|
||||||
|
/// </summary>
|
||||||
|
// TODO: In order to authenticate this in the future, Dlna playback will require updating
|
||||||
|
public class AudioController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly AudioHelper _audioHelper;
|
||||||
|
|
||||||
|
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The audio controller.
|
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
// TODO: In order to authenticate this in the future, Dlna playback will require updating
|
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
||||||
public class AudioController : BaseJellyfinApiController
|
public AudioController(AudioHelper audioHelper)
|
||||||
{
|
{
|
||||||
private readonly AudioHelper _audioHelper;
|
_audioHelper = audioHelper;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
|
/// <summary>
|
||||||
|
/// Gets an audio stream.
|
||||||
/// <summary>
|
/// </summary>
|
||||||
/// Initializes a new instance of the <see cref="AudioController"/> class.
|
/// <param name="itemId">The item id.</param>
|
||||||
/// </summary>
|
/// <param name="container">The audio container.</param>
|
||||||
/// <param name="audioHelper">Instance of <see cref="AudioHelper"/>.</param>
|
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||||
public AudioController(AudioHelper audioHelper)
|
/// <param name="params">The streaming parameters.</param>
|
||||||
|
/// <param name="tag">The tag.</param>
|
||||||
|
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||||
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
|
/// <param name="segmentLength">The segment length.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
|
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||||
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
|
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||||
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
|
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||||
|
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||||
|
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||||
|
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||||
|
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||||
|
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||||
|
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||||
|
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||||
|
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||||
|
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||||
|
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||||
|
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||||
|
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||||
|
/// <param name="maxRefFrames">Optional.</param>
|
||||||
|
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||||
|
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||||
|
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</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="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||||
|
/// <param name="liveStreamId">The live stream id.</param>
|
||||||
|
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||||
|
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||||
|
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||||
|
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||||
|
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||||
|
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||||
|
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||||
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
|
/// <response code="200">Audio stream returned.</response>
|
||||||
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
|
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
||||||
|
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesAudioFile]
|
||||||
|
public async Task<ActionResult> GetAudioStream(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromQuery] string? container,
|
||||||
|
[FromQuery] bool? @static,
|
||||||
|
[FromQuery] string? @params,
|
||||||
|
[FromQuery] string? tag,
|
||||||
|
[FromQuery] string? deviceProfileId,
|
||||||
|
[FromQuery] string? playSessionId,
|
||||||
|
[FromQuery] string? segmentContainer,
|
||||||
|
[FromQuery] int? segmentLength,
|
||||||
|
[FromQuery] int? minSegments,
|
||||||
|
[FromQuery] string? mediaSourceId,
|
||||||
|
[FromQuery] string? deviceId,
|
||||||
|
[FromQuery] string? audioCodec,
|
||||||
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
|
[FromQuery] bool? breakOnNonKeyFrames,
|
||||||
|
[FromQuery] int? audioSampleRate,
|
||||||
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
|
[FromQuery] int? audioBitRate,
|
||||||
|
[FromQuery] int? audioChannels,
|
||||||
|
[FromQuery] int? maxAudioChannels,
|
||||||
|
[FromQuery] string? profile,
|
||||||
|
[FromQuery] string? level,
|
||||||
|
[FromQuery] float? framerate,
|
||||||
|
[FromQuery] float? maxFramerate,
|
||||||
|
[FromQuery] bool? copyTimestamps,
|
||||||
|
[FromQuery] long? startTimeTicks,
|
||||||
|
[FromQuery] int? width,
|
||||||
|
[FromQuery] int? height,
|
||||||
|
[FromQuery] int? videoBitRate,
|
||||||
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
|
[FromQuery] int? maxRefFrames,
|
||||||
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
|
[FromQuery] bool? requireAvc,
|
||||||
|
[FromQuery] bool? deInterlace,
|
||||||
|
[FromQuery] bool? requireNonAnamorphic,
|
||||||
|
[FromQuery] int? transcodingMaxAudioChannels,
|
||||||
|
[FromQuery] int? cpuCoreLimit,
|
||||||
|
[FromQuery] string? liveStreamId,
|
||||||
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
|
[FromQuery] string? videoCodec,
|
||||||
|
[FromQuery] string? subtitleCodec,
|
||||||
|
[FromQuery] string? transcodeReasons,
|
||||||
|
[FromQuery] int? audioStreamIndex,
|
||||||
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
[FromQuery] EncodingContext? context,
|
||||||
|
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||||
|
{
|
||||||
|
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||||
{
|
{
|
||||||
_audioHelper = audioHelper;
|
Id = itemId,
|
||||||
}
|
Container = container,
|
||||||
|
Static = @static ?? false,
|
||||||
|
Params = @params,
|
||||||
|
Tag = tag,
|
||||||
|
DeviceProfileId = deviceProfileId,
|
||||||
|
PlaySessionId = playSessionId,
|
||||||
|
SegmentContainer = segmentContainer,
|
||||||
|
SegmentLength = segmentLength,
|
||||||
|
MinSegments = minSegments,
|
||||||
|
MediaSourceId = mediaSourceId,
|
||||||
|
DeviceId = deviceId,
|
||||||
|
AudioCodec = audioCodec,
|
||||||
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
|
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||||
|
AudioSampleRate = audioSampleRate,
|
||||||
|
MaxAudioChannels = maxAudioChannels,
|
||||||
|
AudioBitRate = audioBitRate,
|
||||||
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
|
AudioChannels = audioChannels,
|
||||||
|
Profile = profile,
|
||||||
|
Level = level,
|
||||||
|
Framerate = framerate,
|
||||||
|
MaxFramerate = maxFramerate,
|
||||||
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
|
StartTimeTicks = startTimeTicks,
|
||||||
|
Width = width,
|
||||||
|
Height = height,
|
||||||
|
VideoBitRate = videoBitRate,
|
||||||
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
|
MaxRefFrames = maxRefFrames,
|
||||||
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
|
RequireAvc = requireAvc ?? false,
|
||||||
|
DeInterlace = deInterlace ?? false,
|
||||||
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
|
LiveStreamId = liveStreamId,
|
||||||
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
|
VideoCodec = videoCodec,
|
||||||
|
SubtitleCodec = subtitleCodec,
|
||||||
|
TranscodeReasons = transcodeReasons,
|
||||||
|
AudioStreamIndex = audioStreamIndex,
|
||||||
|
VideoStreamIndex = videoStreamIndex,
|
||||||
|
Context = context ?? EncodingContext.Static,
|
||||||
|
StreamOptions = streamOptions
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||||
/// Gets an audio stream.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <summary>
|
||||||
/// <param name="container">The audio container.</param>
|
/// Gets an audio stream.
|
||||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
/// </summary>
|
||||||
/// <param name="params">The streaming parameters.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="tag">The tag.</param>
|
/// <param name="container">The audio container.</param>
|
||||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
||||||
/// <param name="playSessionId">The play session id.</param>
|
/// <param name="params">The streaming parameters.</param>
|
||||||
/// <param name="segmentContainer">The segment container.</param>
|
/// <param name="tag">The tag.</param>
|
||||||
/// <param name="segmentLength">The segment length.</param>
|
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
||||||
/// <param name="minSegments">The minimum number of segments.</param>
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
/// <param name="segmentLength">The segment length.</param>
|
||||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
/// <param name="minSegments">The minimum number of segments.</param>
|
||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
/// <param name="mediaSourceId">The media version id, if playing an alternate version.</param>
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
||||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
||||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
||||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
||||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
||||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
||||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
||||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
||||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
||||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
||||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
||||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
||||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
||||||
/// <param name="maxRefFrames">Optional.</param>
|
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
||||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
||||||
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
||||||
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
/// <param name="maxRefFrames">Optional.</param>
|
||||||
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamorphic stream.</param>
|
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
||||||
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
/// <param name="requireAvc">Optional. Whether to require avc.</param>
|
||||||
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
/// <param name="deInterlace">Optional. Whether to deinterlace the video.</param>
|
||||||
/// <param name="liveStreamId">The live stream id.</param>
|
/// <param name="requireNonAnamorphic">Optional. Whether to require a non anamporphic stream.</param>
|
||||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
/// <param name="transcodingMaxAudioChannels">Optional. The maximum number of audio channels to transcode.</param>
|
||||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
/// <param name="cpuCoreLimit">Optional. The limit of how many cpu cores to use.</param>
|
||||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
/// <param name="liveStreamId">The live stream id.</param>
|
||||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
||||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
||||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
||||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
||||||
/// <response code="200">Audio stream returned.</response>
|
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||||
[HttpGet("{itemId}/stream", Name = "GetAudioStream")]
|
/// <param name="streamOptions">Optional. The streaming options.</param>
|
||||||
[HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
|
/// <response code="200">Audio stream returned.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
||||||
[ProducesAudioFile]
|
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
|
||||||
public async Task<ActionResult> GetAudioStream(
|
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
|
||||||
[FromRoute, Required] Guid itemId,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[FromQuery] string? container,
|
[ProducesAudioFile]
|
||||||
[FromQuery] bool? @static,
|
public async Task<ActionResult> GetAudioStreamByContainer(
|
||||||
[FromQuery] string? @params,
|
[FromRoute, Required] Guid itemId,
|
||||||
[FromQuery] string? tag,
|
[FromRoute, Required] string container,
|
||||||
[FromQuery] string? deviceProfileId,
|
[FromQuery] bool? @static,
|
||||||
[FromQuery] string? playSessionId,
|
[FromQuery] string? @params,
|
||||||
[FromQuery] string? segmentContainer,
|
[FromQuery] string? tag,
|
||||||
[FromQuery] int? segmentLength,
|
[FromQuery] string? deviceProfileId,
|
||||||
[FromQuery] int? minSegments,
|
[FromQuery] string? playSessionId,
|
||||||
[FromQuery] string? mediaSourceId,
|
[FromQuery] string? segmentContainer,
|
||||||
[FromQuery] string? deviceId,
|
[FromQuery] int? segmentLength,
|
||||||
[FromQuery] string? audioCodec,
|
[FromQuery] int? minSegments,
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
[FromQuery] string? mediaSourceId,
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
[FromQuery] string? deviceId,
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
[FromQuery] string? audioCodec,
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
[FromQuery] bool? enableAutoStreamCopy,
|
||||||
[FromQuery] int? audioSampleRate,
|
[FromQuery] bool? allowVideoStreamCopy,
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
[FromQuery] bool? allowAudioStreamCopy,
|
||||||
[FromQuery] int? audioBitRate,
|
[FromQuery] bool? breakOnNonKeyFrames,
|
||||||
[FromQuery] int? audioChannels,
|
[FromQuery] int? audioSampleRate,
|
||||||
[FromQuery] int? maxAudioChannels,
|
[FromQuery] int? maxAudioBitDepth,
|
||||||
[FromQuery] string? profile,
|
[FromQuery] int? audioBitRate,
|
||||||
[FromQuery] string? level,
|
[FromQuery] int? audioChannels,
|
||||||
[FromQuery] float? framerate,
|
[FromQuery] int? maxAudioChannels,
|
||||||
[FromQuery] float? maxFramerate,
|
[FromQuery] string? profile,
|
||||||
[FromQuery] bool? copyTimestamps,
|
[FromQuery] string? level,
|
||||||
[FromQuery] long? startTimeTicks,
|
[FromQuery] float? framerate,
|
||||||
[FromQuery] int? width,
|
[FromQuery] float? maxFramerate,
|
||||||
[FromQuery] int? height,
|
[FromQuery] bool? copyTimestamps,
|
||||||
[FromQuery] int? videoBitRate,
|
[FromQuery] long? startTimeTicks,
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
[FromQuery] int? width,
|
||||||
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
[FromQuery] int? height,
|
||||||
[FromQuery] int? maxRefFrames,
|
[FromQuery] int? videoBitRate,
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
[FromQuery] int? subtitleStreamIndex,
|
||||||
[FromQuery] bool? requireAvc,
|
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
||||||
[FromQuery] bool? deInterlace,
|
[FromQuery] int? maxRefFrames,
|
||||||
[FromQuery] bool? requireNonAnamorphic,
|
[FromQuery] int? maxVideoBitDepth,
|
||||||
[FromQuery] int? transcodingMaxAudioChannels,
|
[FromQuery] bool? requireAvc,
|
||||||
[FromQuery] int? cpuCoreLimit,
|
[FromQuery] bool? deInterlace,
|
||||||
[FromQuery] string? liveStreamId,
|
[FromQuery] bool? requireNonAnamorphic,
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
[FromQuery] int? transcodingMaxAudioChannels,
|
||||||
[FromQuery] string? videoCodec,
|
[FromQuery] int? cpuCoreLimit,
|
||||||
[FromQuery] string? subtitleCodec,
|
[FromQuery] string? liveStreamId,
|
||||||
[FromQuery] string? transcodeReasons,
|
[FromQuery] bool? enableMpegtsM2TsMode,
|
||||||
[FromQuery] int? audioStreamIndex,
|
[FromQuery] string? videoCodec,
|
||||||
[FromQuery] int? videoStreamIndex,
|
[FromQuery] string? subtitleCodec,
|
||||||
[FromQuery] EncodingContext? context,
|
[FromQuery] string? transcodeReasons,
|
||||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
[FromQuery] int? audioStreamIndex,
|
||||||
|
[FromQuery] int? videoStreamIndex,
|
||||||
|
[FromQuery] EncodingContext? context,
|
||||||
|
[FromQuery] Dictionary<string, string>? streamOptions)
|
||||||
|
{
|
||||||
|
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
||||||
{
|
{
|
||||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
Id = itemId,
|
||||||
{
|
Container = container,
|
||||||
Id = itemId,
|
Static = @static ?? false,
|
||||||
Container = container,
|
Params = @params,
|
||||||
Static = @static ?? false,
|
Tag = tag,
|
||||||
Params = @params,
|
DeviceProfileId = deviceProfileId,
|
||||||
Tag = tag,
|
PlaySessionId = playSessionId,
|
||||||
DeviceProfileId = deviceProfileId,
|
SegmentContainer = segmentContainer,
|
||||||
PlaySessionId = playSessionId,
|
SegmentLength = segmentLength,
|
||||||
SegmentContainer = segmentContainer,
|
MinSegments = minSegments,
|
||||||
SegmentLength = segmentLength,
|
MediaSourceId = mediaSourceId,
|
||||||
MinSegments = minSegments,
|
DeviceId = deviceId,
|
||||||
MediaSourceId = mediaSourceId,
|
AudioCodec = audioCodec,
|
||||||
DeviceId = deviceId,
|
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
||||||
AudioCodec = audioCodec,
|
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
AudioSampleRate = audioSampleRate,
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
MaxAudioChannels = maxAudioChannels,
|
||||||
AudioSampleRate = audioSampleRate,
|
AudioBitRate = audioBitRate,
|
||||||
MaxAudioChannels = maxAudioChannels,
|
MaxAudioBitDepth = maxAudioBitDepth,
|
||||||
AudioBitRate = audioBitRate,
|
AudioChannels = audioChannels,
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
Profile = profile,
|
||||||
AudioChannels = audioChannels,
|
Level = level,
|
||||||
Profile = profile,
|
Framerate = framerate,
|
||||||
Level = level,
|
MaxFramerate = maxFramerate,
|
||||||
Framerate = framerate,
|
CopyTimestamps = copyTimestamps ?? false,
|
||||||
MaxFramerate = maxFramerate,
|
StartTimeTicks = startTimeTicks,
|
||||||
CopyTimestamps = copyTimestamps ?? false,
|
Width = width,
|
||||||
StartTimeTicks = startTimeTicks,
|
Height = height,
|
||||||
Width = width,
|
VideoBitRate = videoBitRate,
|
||||||
Height = height,
|
SubtitleStreamIndex = subtitleStreamIndex,
|
||||||
VideoBitRate = videoBitRate,
|
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
MaxRefFrames = maxRefFrames,
|
||||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
MaxVideoBitDepth = maxVideoBitDepth,
|
||||||
MaxRefFrames = maxRefFrames,
|
RequireAvc = requireAvc ?? false,
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
DeInterlace = deInterlace ?? false,
|
||||||
RequireAvc = requireAvc ?? false,
|
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
||||||
DeInterlace = deInterlace ?? false,
|
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
CpuCoreLimit = cpuCoreLimit,
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
LiveStreamId = liveStreamId,
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
||||||
LiveStreamId = liveStreamId,
|
VideoCodec = videoCodec,
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
SubtitleCodec = subtitleCodec,
|
||||||
VideoCodec = videoCodec,
|
TranscodeReasons = transcodeReasons,
|
||||||
SubtitleCodec = subtitleCodec,
|
AudioStreamIndex = audioStreamIndex,
|
||||||
TranscodeReasons = transcodeReasons,
|
VideoStreamIndex = videoStreamIndex,
|
||||||
AudioStreamIndex = audioStreamIndex,
|
Context = context ?? EncodingContext.Static,
|
||||||
VideoStreamIndex = videoStreamIndex,
|
StreamOptions = streamOptions
|
||||||
Context = context ?? EncodingContext.Static,
|
};
|
||||||
StreamOptions = streamOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets an audio stream.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <param name="container">The audio container.</param>
|
|
||||||
/// <param name="static">Optional. If true, the original file will be streamed statically without any encoding. Use either no url extension or the original file extension. true/false.</param>
|
|
||||||
/// <param name="params">The streaming parameters.</param>
|
|
||||||
/// <param name="tag">The tag.</param>
|
|
||||||
/// <param name="deviceProfileId">Optional. The dlna device profile id to utilize.</param>
|
|
||||||
/// <param name="playSessionId">The play session id.</param>
|
|
||||||
/// <param name="segmentContainer">The segment container.</param>
|
|
||||||
/// <param name="segmentLength">The segment length.</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="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
|
||||||
/// <param name="audioCodec">Optional. Specify a audio codec to encode to, e.g. mp3. If omitted the server will auto-select using the url's extension. Options: aac, mp3, vorbis, wma.</param>
|
|
||||||
/// <param name="enableAutoStreamCopy">Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.</param>
|
|
||||||
/// <param name="allowVideoStreamCopy">Whether or not to allow copying of the video stream url.</param>
|
|
||||||
/// <param name="allowAudioStreamCopy">Whether or not to allow copying of the audio stream url.</param>
|
|
||||||
/// <param name="breakOnNonKeyFrames">Optional. Whether to break on non key frames.</param>
|
|
||||||
/// <param name="audioSampleRate">Optional. Specify a specific audio sample rate, e.g. 44100.</param>
|
|
||||||
/// <param name="maxAudioBitDepth">Optional. The maximum audio bit depth.</param>
|
|
||||||
/// <param name="audioBitRate">Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.</param>
|
|
||||||
/// <param name="audioChannels">Optional. Specify a specific number of audio channels to encode to, e.g. 2.</param>
|
|
||||||
/// <param name="maxAudioChannels">Optional. Specify a maximum number of audio channels to encode to, e.g. 2.</param>
|
|
||||||
/// <param name="profile">Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.</param>
|
|
||||||
/// <param name="level">Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.</param>
|
|
||||||
/// <param name="framerate">Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
|
||||||
/// <param name="maxFramerate">Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.</param>
|
|
||||||
/// <param name="copyTimestamps">Whether or not to copy timestamps when transcoding with an offset. Defaults to false.</param>
|
|
||||||
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
|
|
||||||
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
|
|
||||||
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
|
|
||||||
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
|
|
||||||
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
|
|
||||||
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
|
|
||||||
/// <param name="maxRefFrames">Optional.</param>
|
|
||||||
/// <param name="maxVideoBitDepth">Optional. The maximum video bit depth.</param>
|
|
||||||
/// <param name="requireAvc">Optional. Whether to require avc.</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="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="liveStreamId">The live stream id.</param>
|
|
||||||
/// <param name="enableMpegtsM2TsMode">Optional. Whether to enable the MpegtsM2Ts mode.</param>
|
|
||||||
/// <param name="videoCodec">Optional. Specify a video codec to encode to, e.g. h264. If omitted the server will auto-select using the url's extension. Options: h265, h264, mpeg4, theora, vp8, vp9, vpx (deprecated), wmv.</param>
|
|
||||||
/// <param name="subtitleCodec">Optional. Specify a subtitle codec to encode to.</param>
|
|
||||||
/// <param name="transcodeReasons">Optional. The transcoding reason.</param>
|
|
||||||
/// <param name="audioStreamIndex">Optional. The index of the audio stream to use. If omitted the first audio stream will be used.</param>
|
|
||||||
/// <param name="videoStreamIndex">Optional. The index of the video stream to use. If omitted the first video stream will be used.</param>
|
|
||||||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
|
||||||
/// <param name="streamOptions">Optional. The streaming options.</param>
|
|
||||||
/// <response code="200">Audio stream returned.</response>
|
|
||||||
/// <returns>A <see cref="FileResult"/> containing the audio file.</returns>
|
|
||||||
[HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
|
|
||||||
[HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesAudioFile]
|
|
||||||
public async Task<ActionResult> GetAudioStreamByContainer(
|
|
||||||
[FromRoute, Required] Guid itemId,
|
|
||||||
[FromRoute, Required] string container,
|
|
||||||
[FromQuery] bool? @static,
|
|
||||||
[FromQuery] string? @params,
|
|
||||||
[FromQuery] string? tag,
|
|
||||||
[FromQuery] string? deviceProfileId,
|
|
||||||
[FromQuery] string? playSessionId,
|
|
||||||
[FromQuery] string? segmentContainer,
|
|
||||||
[FromQuery] int? segmentLength,
|
|
||||||
[FromQuery] int? minSegments,
|
|
||||||
[FromQuery] string? mediaSourceId,
|
|
||||||
[FromQuery] string? deviceId,
|
|
||||||
[FromQuery] string? audioCodec,
|
|
||||||
[FromQuery] bool? enableAutoStreamCopy,
|
|
||||||
[FromQuery] bool? allowVideoStreamCopy,
|
|
||||||
[FromQuery] bool? allowAudioStreamCopy,
|
|
||||||
[FromQuery] bool? breakOnNonKeyFrames,
|
|
||||||
[FromQuery] int? audioSampleRate,
|
|
||||||
[FromQuery] int? maxAudioBitDepth,
|
|
||||||
[FromQuery] int? audioBitRate,
|
|
||||||
[FromQuery] int? audioChannels,
|
|
||||||
[FromQuery] int? maxAudioChannels,
|
|
||||||
[FromQuery] string? profile,
|
|
||||||
[FromQuery] string? level,
|
|
||||||
[FromQuery] float? framerate,
|
|
||||||
[FromQuery] float? maxFramerate,
|
|
||||||
[FromQuery] bool? copyTimestamps,
|
|
||||||
[FromQuery] long? startTimeTicks,
|
|
||||||
[FromQuery] int? width,
|
|
||||||
[FromQuery] int? height,
|
|
||||||
[FromQuery] int? videoBitRate,
|
|
||||||
[FromQuery] int? subtitleStreamIndex,
|
|
||||||
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
|
|
||||||
[FromQuery] int? maxRefFrames,
|
|
||||||
[FromQuery] int? maxVideoBitDepth,
|
|
||||||
[FromQuery] bool? requireAvc,
|
|
||||||
[FromQuery] bool? deInterlace,
|
|
||||||
[FromQuery] bool? requireNonAnamorphic,
|
|
||||||
[FromQuery] int? transcodingMaxAudioChannels,
|
|
||||||
[FromQuery] int? cpuCoreLimit,
|
|
||||||
[FromQuery] string? liveStreamId,
|
|
||||||
[FromQuery] bool? enableMpegtsM2TsMode,
|
|
||||||
[FromQuery] string? videoCodec,
|
|
||||||
[FromQuery] string? subtitleCodec,
|
|
||||||
[FromQuery] string? transcodeReasons,
|
|
||||||
[FromQuery] int? audioStreamIndex,
|
|
||||||
[FromQuery] int? videoStreamIndex,
|
|
||||||
[FromQuery] EncodingContext? context,
|
|
||||||
[FromQuery] Dictionary<string, string>? streamOptions)
|
|
||||||
{
|
|
||||||
StreamingRequestDto streamingRequest = new StreamingRequestDto
|
|
||||||
{
|
|
||||||
Id = itemId,
|
|
||||||
Container = container,
|
|
||||||
Static = @static ?? false,
|
|
||||||
Params = @params,
|
|
||||||
Tag = tag,
|
|
||||||
DeviceProfileId = deviceProfileId,
|
|
||||||
PlaySessionId = playSessionId,
|
|
||||||
SegmentContainer = segmentContainer,
|
|
||||||
SegmentLength = segmentLength,
|
|
||||||
MinSegments = minSegments,
|
|
||||||
MediaSourceId = mediaSourceId,
|
|
||||||
DeviceId = deviceId,
|
|
||||||
AudioCodec = audioCodec,
|
|
||||||
EnableAutoStreamCopy = enableAutoStreamCopy ?? true,
|
|
||||||
AllowAudioStreamCopy = allowAudioStreamCopy ?? true,
|
|
||||||
AllowVideoStreamCopy = allowVideoStreamCopy ?? true,
|
|
||||||
BreakOnNonKeyFrames = breakOnNonKeyFrames ?? false,
|
|
||||||
AudioSampleRate = audioSampleRate,
|
|
||||||
MaxAudioChannels = maxAudioChannels,
|
|
||||||
AudioBitRate = audioBitRate,
|
|
||||||
MaxAudioBitDepth = maxAudioBitDepth,
|
|
||||||
AudioChannels = audioChannels,
|
|
||||||
Profile = profile,
|
|
||||||
Level = level,
|
|
||||||
Framerate = framerate,
|
|
||||||
MaxFramerate = maxFramerate,
|
|
||||||
CopyTimestamps = copyTimestamps ?? false,
|
|
||||||
StartTimeTicks = startTimeTicks,
|
|
||||||
Width = width,
|
|
||||||
Height = height,
|
|
||||||
VideoBitRate = videoBitRate,
|
|
||||||
SubtitleStreamIndex = subtitleStreamIndex,
|
|
||||||
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
|
|
||||||
MaxRefFrames = maxRefFrames,
|
|
||||||
MaxVideoBitDepth = maxVideoBitDepth,
|
|
||||||
RequireAvc = requireAvc ?? false,
|
|
||||||
DeInterlace = deInterlace ?? false,
|
|
||||||
RequireNonAnamorphic = requireNonAnamorphic ?? false,
|
|
||||||
TranscodingMaxAudioChannels = transcodingMaxAudioChannels,
|
|
||||||
CpuCoreLimit = cpuCoreLimit,
|
|
||||||
LiveStreamId = liveStreamId,
|
|
||||||
EnableMpegtsM2TsMode = enableMpegtsM2TsMode ?? false,
|
|
||||||
VideoCodec = videoCodec,
|
|
||||||
SubtitleCodec = subtitleCodec,
|
|
||||||
TranscodeReasons = transcodeReasons,
|
|
||||||
AudioStreamIndex = audioStreamIndex,
|
|
||||||
VideoStreamIndex = videoStreamIndex,
|
|
||||||
Context = context ?? EncodingContext.Static,
|
|
||||||
StreamOptions = streamOptions
|
|
||||||
};
|
|
||||||
|
|
||||||
return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Branding controller.
|
||||||
|
/// </summary>
|
||||||
|
public class BrandingController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Branding controller.
|
/// Initializes a new instance of the <see cref="BrandingController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class BrandingController : BaseJellyfinApiController
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public BrandingController(IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="BrandingController"/> class.
|
/// Gets branding configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="200">Branding configuration returned.</response>
|
||||||
public BrandingController(IServerConfigurationManager serverConfigurationManager)
|
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
|
||||||
{
|
[HttpGet("Configuration")]
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
}
|
public ActionResult<BrandingOptions> GetBrandingOptions()
|
||||||
|
{
|
||||||
|
return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets branding configuration.
|
/// Gets branding css.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Branding configuration returned.</response>
|
/// <response code="200">Branding css returned.</response>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the branding configuration.</returns>
|
/// <response code="204">No branding css configured.</response>
|
||||||
[HttpGet("Configuration")]
|
/// <returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// An <see cref="OkResult"/> containing the branding css if exist,
|
||||||
public ActionResult<BrandingOptions> GetBrandingOptions()
|
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
||||||
{
|
/// </returns>
|
||||||
return _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
[HttpGet("Css")]
|
||||||
}
|
[HttpGet("Css.css", Name = "GetBrandingCss_2")]
|
||||||
|
[Produces("text/css")]
|
||||||
/// <summary>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
/// Gets branding css.
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
/// </summary>
|
public ActionResult<string> GetBrandingCss()
|
||||||
/// <response code="200">Branding css returned.</response>
|
{
|
||||||
/// <response code="204">No branding css configured.</response>
|
var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||||
/// <returns>
|
return options.CustomCss ?? string.Empty;
|
||||||
/// An <see cref="OkResult"/> containing the branding css if exist,
|
|
||||||
/// or a <see cref="NoContentResult"/> if the css is not configured.
|
|
||||||
/// </returns>
|
|
||||||
[HttpGet("Css")]
|
|
||||||
[HttpGet("Css.css", Name = "GetBrandingCss_2")]
|
|
||||||
[Produces("text/css")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public ActionResult<string> GetBrandingCss()
|
|
||||||
{
|
|
||||||
var options = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
|
||||||
return options.CustomCss ?? string.Empty;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
@ -18,234 +17,233 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Channels Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
public class ChannelsController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IChannelManager _channelManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Channels Controller.
|
/// Initializes a new instance of the <see cref="ChannelsController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
|
||||||
public class ChannelsController : BaseJellyfinApiController
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
|
public ChannelsController(IChannelManager channelManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
private readonly IChannelManager _channelManager;
|
_channelManager = channelManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ChannelsController"/> class.
|
/// Gets available channels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="channelManager">Instance of the <see cref="IChannelManager"/> interface.</param>
|
/// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
public ChannelsController(IChannelManager channelManager, IUserManager userManager)
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
|
||||||
|
/// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
|
||||||
|
/// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
|
||||||
|
/// <response code="200">Channels returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery] bool? supportsLatestItems,
|
||||||
|
[FromQuery] bool? supportsMediaDeletion,
|
||||||
|
[FromQuery] bool? isFavorite)
|
||||||
|
{
|
||||||
|
return _channelManager.GetChannels(new ChannelQuery
|
||||||
{
|
{
|
||||||
_channelManager = channelManager;
|
Limit = limit,
|
||||||
_userManager = userManager;
|
StartIndex = startIndex,
|
||||||
}
|
UserId = userId ?? Guid.Empty,
|
||||||
|
SupportsLatestItems = supportsLatestItems,
|
||||||
|
SupportsMediaDeletion = supportsMediaDeletion,
|
||||||
|
IsFavorite = isFavorite
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets available channels.
|
/// Get all channel features.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userId">User Id to filter by. Use <see cref="Guid.Empty"/> to not filter by user.</param>
|
/// <response code="200">All channel features returned.</response>
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
[HttpGet("Features")]
|
||||||
/// <param name="supportsLatestItems">Optional. Filter by channels that support getting latest items.</param>
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
/// <param name="supportsMediaDeletion">Optional. Filter by channels that support media deletion.</param>
|
public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
|
||||||
/// <param name="isFavorite">Optional. Filter by channels that are favorite.</param>
|
{
|
||||||
/// <response code="200">Channels returned.</response>
|
return _channelManager.GetAllChannelFeatures();
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channels.</returns>
|
}
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <summary>
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetChannels(
|
/// Get channel features.
|
||||||
[FromQuery] Guid? userId,
|
/// </summary>
|
||||||
[FromQuery] int? startIndex,
|
/// <param name="channelId">Channel id.</param>
|
||||||
[FromQuery] int? limit,
|
/// <response code="200">Channel features returned.</response>
|
||||||
[FromQuery] bool? supportsLatestItems,
|
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
||||||
[FromQuery] bool? supportsMediaDeletion,
|
[HttpGet("{channelId}/Features")]
|
||||||
[FromQuery] bool? isFavorite)
|
public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
|
||||||
|
{
|
||||||
|
return _channelManager.GetChannelFeatures(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get channel items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelId">Channel Id.</param>
|
||||||
|
/// <param name="folderId">Optional. Folder Id.</param>
|
||||||
|
/// <param name="userId">Optional. User Id.</param>
|
||||||
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
|
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
|
||||||
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
|
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
||||||
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
|
/// <response code="200">Channel items returned.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task"/> representing the request to get the channel items.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the channel items.
|
||||||
|
/// </returns>
|
||||||
|
[HttpGet("{channelId}/Items")]
|
||||||
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
|
||||||
|
[FromRoute, Required] Guid channelId,
|
||||||
|
[FromQuery] Guid? folderId,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
||||||
|
{
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
return _channelManager.GetChannels(new ChannelQuery
|
Limit = limit,
|
||||||
|
StartIndex = startIndex,
|
||||||
|
ChannelIds = new[] { channelId },
|
||||||
|
ParentId = folderId ?? Guid.Empty,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
||||||
|
DtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
switch (filter)
|
||||||
{
|
{
|
||||||
Limit = limit,
|
case ItemFilter.IsFolder:
|
||||||
StartIndex = startIndex,
|
query.IsFolder = true;
|
||||||
UserId = userId ?? Guid.Empty,
|
break;
|
||||||
SupportsLatestItems = supportsLatestItems,
|
case ItemFilter.IsNotFolder:
|
||||||
SupportsMediaDeletion = supportsMediaDeletion,
|
query.IsFolder = false;
|
||||||
IsFavorite = isFavorite
|
break;
|
||||||
});
|
case ItemFilter.IsUnplayed:
|
||||||
}
|
query.IsPlayed = false;
|
||||||
|
break;
|
||||||
/// <summary>
|
case ItemFilter.IsPlayed:
|
||||||
/// Get all channel features.
|
query.IsPlayed = true;
|
||||||
/// </summary>
|
break;
|
||||||
/// <response code="200">All channel features returned.</response>
|
case ItemFilter.IsFavorite:
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
query.IsFavorite = true;
|
||||||
[HttpGet("Features")]
|
break;
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
case ItemFilter.IsResumable:
|
||||||
public ActionResult<IEnumerable<ChannelFeatures>> GetAllChannelFeatures()
|
query.IsResumable = true;
|
||||||
{
|
break;
|
||||||
return _channelManager.GetAllChannelFeatures();
|
case ItemFilter.Likes:
|
||||||
}
|
query.IsLiked = true;
|
||||||
|
break;
|
||||||
/// <summary>
|
case ItemFilter.Dislikes:
|
||||||
/// Get channel features.
|
query.IsLiked = false;
|
||||||
/// </summary>
|
break;
|
||||||
/// <param name="channelId">Channel id.</param>
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
/// <response code="200">Channel features returned.</response>
|
query.IsFavoriteOrLiked = true;
|
||||||
/// <returns>An <see cref="OkResult"/> containing the channel features.</returns>
|
break;
|
||||||
[HttpGet("{channelId}/Features")]
|
|
||||||
public ActionResult<ChannelFeatures> GetChannelFeatures([FromRoute, Required] Guid channelId)
|
|
||||||
{
|
|
||||||
return _channelManager.GetChannelFeatures(channelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get channel items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channelId">Channel Id.</param>
|
|
||||||
/// <param name="folderId">Optional. Folder Id.</param>
|
|
||||||
/// <param name="userId">Optional. User Id.</param>
|
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
|
||||||
/// <param name="sortOrder">Optional. Sort Order - Ascending,Descending.</param>
|
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
|
||||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited. Options: Album, AlbumArtist, Artist, Budget, CommunityRating, CriticRating, DateCreated, DatePlayed, PlayCount, PremiereDate, ProductionYear, SortName, Random, Revenue, Runtime.</param>
|
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
|
||||||
/// <response code="200">Channel items returned.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task"/> representing the request to get the channel items.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the channel items.
|
|
||||||
/// </returns>
|
|
||||||
[HttpGet("{channelId}/Items")]
|
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetChannelItems(
|
|
||||||
[FromRoute, Required] Guid channelId,
|
|
||||||
[FromQuery] Guid? folderId,
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] int? startIndex,
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
|
|
||||||
{
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
Limit = limit,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
ChannelIds = new[] { channelId },
|
|
||||||
ParentId = folderId ?? Guid.Empty,
|
|
||||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder),
|
|
||||||
DtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var filter in filters)
|
|
||||||
{
|
|
||||||
switch (filter)
|
|
||||||
{
|
|
||||||
case ItemFilter.IsFolder:
|
|
||||||
query.IsFolder = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsNotFolder:
|
|
||||||
query.IsFolder = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsUnplayed:
|
|
||||||
query.IsPlayed = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsPlayed:
|
|
||||||
query.IsPlayed = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavorite:
|
|
||||||
query.IsFavorite = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsResumable:
|
|
||||||
query.IsResumable = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.Likes:
|
|
||||||
query.IsLiked = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.Dislikes:
|
|
||||||
query.IsLiked = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavoriteOrLikes:
|
|
||||||
query.IsFavoriteOrLiked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||||
/// Gets latest channel items.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">Optional. User Id.</param>
|
/// <summary>
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
/// Gets latest channel items.
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
/// </summary>
|
||||||
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
/// <param name="userId">Optional. User Id.</param>
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <response code="200">Latest channel items returned.</response>
|
/// <param name="filters">Optional. Specify additional filters to apply.</param>
|
||||||
/// <returns>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
/// A <see cref="Task"/> representing the request to get the latest channel items.
|
/// <param name="channelIds">Optional. Specify one or more channel id's, comma delimited.</param>
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the latest channel items.
|
/// <response code="200">Latest channel items returned.</response>
|
||||||
/// </returns>
|
/// <returns>
|
||||||
[HttpGet("Items/Latest")]
|
/// A <see cref="Task"/> representing the request to get the latest channel items.
|
||||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
|
/// The task result contains an <see cref="OkResult"/> containing the latest channel items.
|
||||||
[FromQuery] Guid? userId,
|
/// </returns>
|
||||||
[FromQuery] int? startIndex,
|
[HttpGet("Items/Latest")]
|
||||||
[FromQuery] int? limit,
|
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetLatestChannelItems(
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
[FromQuery] Guid? userId,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
[FromQuery] int? startIndex,
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
|
||||||
|
{
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
Limit = limit,
|
||||||
? null
|
StartIndex = startIndex,
|
||||||
: _userManager.GetUserById(userId.Value);
|
ChannelIds = channelIds,
|
||||||
|
DtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
};
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
foreach (var filter in filters)
|
||||||
|
{
|
||||||
|
switch (filter)
|
||||||
{
|
{
|
||||||
Limit = limit,
|
case ItemFilter.IsFolder:
|
||||||
StartIndex = startIndex,
|
query.IsFolder = true;
|
||||||
ChannelIds = channelIds,
|
break;
|
||||||
DtoOptions = new DtoOptions { Fields = fields }
|
case ItemFilter.IsNotFolder:
|
||||||
};
|
query.IsFolder = false;
|
||||||
|
break;
|
||||||
foreach (var filter in filters)
|
case ItemFilter.IsUnplayed:
|
||||||
{
|
query.IsPlayed = false;
|
||||||
switch (filter)
|
break;
|
||||||
{
|
case ItemFilter.IsPlayed:
|
||||||
case ItemFilter.IsFolder:
|
query.IsPlayed = true;
|
||||||
query.IsFolder = true;
|
break;
|
||||||
break;
|
case ItemFilter.IsFavorite:
|
||||||
case ItemFilter.IsNotFolder:
|
query.IsFavorite = true;
|
||||||
query.IsFolder = false;
|
break;
|
||||||
break;
|
case ItemFilter.IsResumable:
|
||||||
case ItemFilter.IsUnplayed:
|
query.IsResumable = true;
|
||||||
query.IsPlayed = false;
|
break;
|
||||||
break;
|
case ItemFilter.Likes:
|
||||||
case ItemFilter.IsPlayed:
|
query.IsLiked = true;
|
||||||
query.IsPlayed = true;
|
break;
|
||||||
break;
|
case ItemFilter.Dislikes:
|
||||||
case ItemFilter.IsFavorite:
|
query.IsLiked = false;
|
||||||
query.IsFavorite = true;
|
break;
|
||||||
break;
|
case ItemFilter.IsFavoriteOrLikes:
|
||||||
case ItemFilter.IsResumable:
|
query.IsFavoriteOrLiked = true;
|
||||||
query.IsResumable = true;
|
break;
|
||||||
break;
|
|
||||||
case ItemFilter.Likes:
|
|
||||||
query.IsLiked = true;
|
|
||||||
break;
|
|
||||||
case ItemFilter.Dislikes:
|
|
||||||
query.IsLiked = false;
|
|
||||||
break;
|
|
||||||
case ItemFilter.IsFavoriteOrLikes:
|
|
||||||
query.IsFavoriteOrLiked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
|
||||||
using Jellyfin.Api.Models.ClientLogDtos;
|
using Jellyfin.Api.Models.ClientLogDtos;
|
||||||
using MediaBrowser.Controller.ClientEvent;
|
using MediaBrowser.Controller.ClientEvent;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@ -11,71 +9,70 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client log controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
public class ClientLogController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private const int MaxDocumentSize = 1_000_000;
|
||||||
|
private readonly IClientEventLogger _clientEventLogger;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Client log controller.
|
/// Initializes a new instance of the <see cref="ClientLogController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
|
||||||
public class ClientLogController : BaseJellyfinApiController
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public ClientLogController(
|
||||||
|
IClientEventLogger clientEventLogger,
|
||||||
|
IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
private const int MaxDocumentSize = 1_000_000;
|
_clientEventLogger = clientEventLogger;
|
||||||
private readonly IClientEventLogger _clientEventLogger;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ClientLogController"/> class.
|
/// Upload a document.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param>
|
/// <response code="200">Document saved.</response>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <response code="403">Event logging disabled.</response>
|
||||||
public ClientLogController(
|
/// <response code="413">Upload size too large.</response>
|
||||||
IClientEventLogger clientEventLogger,
|
/// <returns>Create response.</returns>
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
[HttpPost("Document")]
|
||||||
|
[ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
|
||||||
|
[AcceptsFile(MediaTypeNames.Text.Plain)]
|
||||||
|
[RequestSizeLimit(MaxDocumentSize)]
|
||||||
|
public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
|
||||||
|
{
|
||||||
|
if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
|
||||||
{
|
{
|
||||||
_clientEventLogger = clientEventLogger;
|
return Forbid();
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (Request.ContentLength > MaxDocumentSize)
|
||||||
/// Upload a document.
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Document saved.</response>
|
|
||||||
/// <response code="403">Event logging disabled.</response>
|
|
||||||
/// <response code="413">Upload size too large.</response>
|
|
||||||
/// <returns>Create response.</returns>
|
|
||||||
[HttpPost("Document")]
|
|
||||||
[ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
|
|
||||||
[AcceptsFile(MediaTypeNames.Text.Plain)]
|
|
||||||
[RequestSizeLimit(MaxDocumentSize)]
|
|
||||||
public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile()
|
|
||||||
{
|
{
|
||||||
if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
|
// Manually validate to return proper status code.
|
||||||
{
|
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
|
||||||
return Forbid();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Request.ContentLength > MaxDocumentSize)
|
|
||||||
{
|
|
||||||
// Manually validate to return proper status code.
|
|
||||||
return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
var (clientName, clientVersion) = GetRequestInformation();
|
|
||||||
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(new ClientLogDocumentResponseDto(fileName));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string ClientName, string ClientVersion) GetRequestInformation()
|
var (clientName, clientVersion) = GetRequestInformation();
|
||||||
{
|
var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
|
||||||
var clientName = HttpContext.User.GetClient() ?? "unknown-client";
|
.ConfigureAwait(false);
|
||||||
var clientVersion = HttpContext.User.GetIsApiKey()
|
return Ok(new ClientLogDocumentResponseDto(fileName));
|
||||||
? "apikey"
|
}
|
||||||
: HttpContext.User.GetVersion() ?? "unknown-version";
|
|
||||||
|
|
||||||
return (clientName, clientVersion);
|
private (string ClientName, string ClientVersion) GetRequestInformation()
|
||||||
}
|
{
|
||||||
|
var clientName = HttpContext.User.GetClient() ?? "unknown-client";
|
||||||
|
var clientVersion = HttpContext.User.GetIsApiKey()
|
||||||
|
? "apikey"
|
||||||
|
: HttpContext.User.GetVersion() ?? "unknown-version";
|
||||||
|
|
||||||
|
return (clientName, clientVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The collection controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Collections")]
|
||||||
|
[Authorize(Policy = Policies.CollectionManagement)]
|
||||||
|
public class CollectionController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ICollectionManager _collectionManager;
|
||||||
|
private readonly IDtoService _dtoService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The collection controller.
|
/// Initializes a new instance of the <see cref="CollectionController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Collections")]
|
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
||||||
public class CollectionController : BaseJellyfinApiController
|
public CollectionController(
|
||||||
|
ICollectionManager collectionManager,
|
||||||
|
IDtoService dtoService)
|
||||||
{
|
{
|
||||||
private readonly ICollectionManager _collectionManager;
|
_collectionManager = collectionManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="CollectionController"/> class.
|
/// Creates a new collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="collectionManager">Instance of <see cref="ICollectionManager"/> interface.</param>
|
/// <param name="name">The name of the collection.</param>
|
||||||
/// <param name="dtoService">Instance of <see cref="IDtoService"/> interface.</param>
|
/// <param name="ids">Item Ids to add to the collection.</param>
|
||||||
public CollectionController(
|
/// <param name="parentId">Optional. Create the collection within a specific folder.</param>
|
||||||
ICollectionManager collectionManager,
|
/// <param name="isLocked">Whether or not to lock the new collection.</param>
|
||||||
IDtoService dtoService)
|
/// <response code="200">Collection created.</response>
|
||||||
|
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
|
||||||
|
[HttpPost]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
|
||||||
|
[FromQuery] string? name,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
|
||||||
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery] bool isLocked = false)
|
||||||
|
{
|
||||||
|
var userId = User.GetUserId();
|
||||||
|
|
||||||
|
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
||||||
{
|
{
|
||||||
_collectionManager = collectionManager;
|
IsLocked = isLocked,
|
||||||
_dtoService = dtoService;
|
Name = name,
|
||||||
}
|
ParentId = parentId,
|
||||||
|
ItemIdList = ids,
|
||||||
|
UserIds = new[] { userId }
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>
|
var dtoOptions = new DtoOptions().AddClientFields(User);
|
||||||
/// Creates a new collection.
|
|
||||||
/// </summary>
|
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
/// <param name="name">The name of the collection.</param>
|
|
||||||
/// <param name="ids">Item Ids to add to the collection.</param>
|
return new CollectionCreationResult
|
||||||
/// <param name="parentId">Optional. Create the collection within a specific folder.</param>
|
|
||||||
/// <param name="isLocked">Whether or not to lock the new collection.</param>
|
|
||||||
/// <response code="200">Collection created.</response>
|
|
||||||
/// <returns>A <see cref="CollectionCreationOptions"/> with information about the new collection.</returns>
|
|
||||||
[HttpPost]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<ActionResult<CollectionCreationResult>> CreateCollection(
|
|
||||||
[FromQuery] string? name,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery] bool isLocked = false)
|
|
||||||
{
|
{
|
||||||
var userId = User.GetUserId();
|
Id = dto.Id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
|
/// <summary>
|
||||||
{
|
/// Adds items to a collection.
|
||||||
IsLocked = isLocked,
|
/// </summary>
|
||||||
Name = name,
|
/// <param name="collectionId">The collection id.</param>
|
||||||
ParentId = parentId,
|
/// <param name="ids">Item ids, comma delimited.</param>
|
||||||
ItemIdList = ids,
|
/// <response code="204">Items added to collection.</response>
|
||||||
UserIds = new[] { userId }
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
}).ConfigureAwait(false);
|
[HttpPost("{collectionId}/Items")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> AddToCollection(
|
||||||
|
[FromRoute, Required] Guid collectionId,
|
||||||
|
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||||
|
{
|
||||||
|
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
var dtoOptions = new DtoOptions().AddClientFields(User);
|
/// <summary>
|
||||||
|
/// Removes items from a collection.
|
||||||
var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
|
/// </summary>
|
||||||
|
/// <param name="collectionId">The collection id.</param>
|
||||||
return new CollectionCreationResult
|
/// <param name="ids">Item ids, comma delimited.</param>
|
||||||
{
|
/// <response code="204">Items removed from collection.</response>
|
||||||
Id = dto.Id
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
};
|
[HttpDelete("{collectionId}/Items")]
|
||||||
}
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> RemoveFromCollection(
|
||||||
/// <summary>
|
[FromRoute, Required] Guid collectionId,
|
||||||
/// Adds items to a collection.
|
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
||||||
/// </summary>
|
{
|
||||||
/// <param name="collectionId">The collection id.</param>
|
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
|
||||||
/// <param name="ids">Item ids, comma delimited.</param>
|
return NoContent();
|
||||||
/// <response code="204">Items added to collection.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
|
||||||
[HttpPost("{collectionId}/Items")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> AddToCollection(
|
|
||||||
[FromRoute, Required] Guid collectionId,
|
|
||||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
|
||||||
{
|
|
||||||
await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Removes items from a collection.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="collectionId">The collection id.</param>
|
|
||||||
/// <param name="ids">Item ids, comma delimited.</param>
|
|
||||||
/// <response code="204">Items removed from collection.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
|
||||||
[HttpDelete("{collectionId}/Items")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> RemoveFromCollection(
|
|
||||||
[FromRoute, Required] Guid collectionId,
|
|
||||||
[FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
|
|
||||||
{
|
|
||||||
await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("System")]
|
||||||
|
[Authorize]
|
||||||
|
public class ConfigurationController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IServerConfigurationManager _configurationManager;
|
||||||
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration Controller.
|
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("System")]
|
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
||||||
public class ConfigurationController : BaseJellyfinApiController
|
public ConfigurationController(
|
||||||
|
IServerConfigurationManager configurationManager,
|
||||||
|
IMediaEncoder mediaEncoder)
|
||||||
{
|
{
|
||||||
private readonly IServerConfigurationManager _configurationManager;
|
_configurationManager = configurationManager;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
|
/// <summary>
|
||||||
|
/// Gets application configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">Application configuration returned.</response>
|
||||||
|
/// <returns>Application configuration.</returns>
|
||||||
|
[HttpGet("Configuration")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<ServerConfiguration> GetConfiguration()
|
||||||
|
{
|
||||||
|
return _configurationManager.Configuration;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ConfigurationController"/> class.
|
/// Updates application configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="configuration">Configuration.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
|
/// <response code="204">Configuration updated.</response>
|
||||||
public ConfigurationController(
|
/// <returns>Update status.</returns>
|
||||||
IServerConfigurationManager configurationManager,
|
[HttpPost("Configuration")]
|
||||||
IMediaEncoder mediaEncoder)
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
|
||||||
|
{
|
||||||
|
_configurationManager.ReplaceConfiguration(configuration);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a named configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Configuration key.</param>
|
||||||
|
/// <response code="200">Configuration returned.</response>
|
||||||
|
/// <returns>Configuration.</returns>
|
||||||
|
[HttpGet("Configuration/{key}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesFile(MediaTypeNames.Application.Json)]
|
||||||
|
public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
|
||||||
|
{
|
||||||
|
return _configurationManager.GetConfiguration(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates named configuration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">Configuration key.</param>
|
||||||
|
/// <param name="configuration">Configuration.</param>
|
||||||
|
/// <response code="204">Named configuration updated.</response>
|
||||||
|
/// <returns>Update status.</returns>
|
||||||
|
[HttpPost("Configuration/{key}")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
|
||||||
|
{
|
||||||
|
var configurationType = _configurationManager.GetConfigurationType(key);
|
||||||
|
var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
|
||||||
|
|
||||||
|
if (deserializedConfiguration is null)
|
||||||
{
|
{
|
||||||
_configurationManager = configurationManager;
|
throw new ArgumentException("Body doesn't contain a valid configuration");
|
||||||
_mediaEncoder = mediaEncoder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
||||||
/// Gets application configuration.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <response code="200">Application configuration returned.</response>
|
|
||||||
/// <returns>Application configuration.</returns>
|
|
||||||
[HttpGet("Configuration")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<ServerConfiguration> GetConfiguration()
|
|
||||||
{
|
|
||||||
return _configurationManager.Configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates application configuration.
|
/// Gets a default MetadataOptions object.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="configuration">Configuration.</param>
|
/// <response code="200">Metadata options returned.</response>
|
||||||
/// <response code="204">Configuration updated.</response>
|
/// <returns>Default MetadataOptions.</returns>
|
||||||
/// <returns>Update status.</returns>
|
[HttpGet("Configuration/MetadataOptions/Default")]
|
||||||
[HttpPost("Configuration")]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
|
||||||
public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
|
{
|
||||||
{
|
return new MetadataOptions();
|
||||||
_configurationManager.ReplaceConfiguration(configuration);
|
}
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a named configuration.
|
/// Updates the path to the media encoder.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">Configuration key.</param>
|
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
||||||
/// <response code="200">Configuration returned.</response>
|
/// <response code="204">Media encoder path updated.</response>
|
||||||
/// <returns>Configuration.</returns>
|
/// <returns>Status.</returns>
|
||||||
[HttpGet("Configuration/{key}")]
|
[HttpPost("MediaEncoder/Path")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||||
[ProducesFile(MediaTypeNames.Application.Json)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
public ActionResult<object> GetNamedConfiguration([FromRoute, Required] string key)
|
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
|
||||||
{
|
{
|
||||||
return _configurationManager.GetConfiguration(key);
|
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
||||||
}
|
return NoContent();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates named configuration.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">Configuration key.</param>
|
|
||||||
/// <param name="configuration">Configuration.</param>
|
|
||||||
/// <response code="204">Named configuration updated.</response>
|
|
||||||
/// <returns>Update status.</returns>
|
|
||||||
[HttpPost("Configuration/{key}")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public ActionResult UpdateNamedConfiguration([FromRoute, Required] string key, [FromBody, Required] JsonDocument configuration)
|
|
||||||
{
|
|
||||||
var configurationType = _configurationManager.GetConfigurationType(key);
|
|
||||||
var deserializedConfiguration = configuration.Deserialize(configurationType, _serializerOptions);
|
|
||||||
|
|
||||||
if (deserializedConfiguration is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Body doesn't contain a valid configuration");
|
|
||||||
}
|
|
||||||
|
|
||||||
_configurationManager.SaveConfiguration(key, deserializedConfiguration);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a default MetadataOptions object.
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Metadata options returned.</response>
|
|
||||||
/// <returns>Default MetadataOptions.</returns>
|
|
||||||
[HttpGet("Configuration/MetadataOptions/Default")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<MetadataOptions> GetDefaultMetadataOptions()
|
|
||||||
{
|
|
||||||
return new MetadataOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the path to the media encoder.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
|
|
||||||
/// <response code="204">Media encoder path updated.</response>
|
|
||||||
/// <returns>Status.</returns>
|
|
||||||
[HttpPost("MediaEncoder/Path")]
|
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public ActionResult UpdateMediaEncoderPath([FromBody, Required] MediaEncoderPathDto mediaEncoderPath)
|
|
||||||
{
|
|
||||||
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Mime;
|
using System.Net.Mime;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Models;
|
using Jellyfin.Api.Models;
|
||||||
using MediaBrowser.Common.Plugins;
|
using MediaBrowser.Common.Plugins;
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
@ -14,103 +13,102 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The dashboard controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
public class DashboardController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<DashboardController> _logger;
|
||||||
|
private readonly IPluginManager _pluginManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The dashboard controller.
|
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
||||||
public class DashboardController : BaseJellyfinApiController
|
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
|
||||||
|
public DashboardController(
|
||||||
|
ILogger<DashboardController> logger,
|
||||||
|
IPluginManager pluginManager)
|
||||||
{
|
{
|
||||||
private readonly ILogger<DashboardController> _logger;
|
_logger = logger;
|
||||||
private readonly IPluginManager _pluginManager;
|
_pluginManager = pluginManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DashboardController"/> class.
|
/// Gets the configuration pages.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{DashboardController}"/> interface.</param>
|
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
|
||||||
/// <param name="pluginManager">Instance of <see cref="IPluginManager"/> interface.</param>
|
/// <response code="200">ConfigurationPages returned.</response>
|
||||||
public DashboardController(
|
/// <response code="404">Server still loading.</response>
|
||||||
ILogger<DashboardController> logger,
|
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
||||||
IPluginManager pluginManager)
|
[HttpGet("web/ConfigurationPages")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[Authorize]
|
||||||
|
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
|
||||||
|
[FromQuery] bool? enableInMainMenu)
|
||||||
|
{
|
||||||
|
var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
|
||||||
|
|
||||||
|
if (enableInMainMenu.HasValue)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
||||||
_pluginManager = pluginManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return configPages;
|
||||||
/// Gets the configuration pages.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="enableInMainMenu">Whether to enable in the main menu.</param>
|
/// <summary>
|
||||||
/// <response code="200">ConfigurationPages returned.</response>
|
/// Gets a dashboard configuration page.
|
||||||
/// <response code="404">Server still loading.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
|
/// <param name="name">The name of the page.</param>
|
||||||
[HttpGet("web/ConfigurationPages")]
|
/// <response code="200">ConfigurationPage returned.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="404">Plugin configuration page not found.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
/// <returns>The configuration page.</returns>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
[HttpGet("web/ConfigurationPage")]
|
||||||
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[FromQuery] bool? enableInMainMenu)
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
|
||||||
|
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
|
||||||
|
{
|
||||||
|
var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (altPage is null)
|
||||||
{
|
{
|
||||||
var configPages = _pluginManager.Plugins.SelectMany(GetConfigPages).ToList();
|
return NotFound();
|
||||||
|
|
||||||
if (enableInMainMenu.HasValue)
|
|
||||||
{
|
|
||||||
configPages = configPages.Where(p => p.EnableInMainMenu == enableInMainMenu.Value).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return configPages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
IPlugin plugin = altPage.Item2;
|
||||||
/// Gets a dashboard configuration page.
|
string resourcePath = altPage.Item1.EmbeddedResourcePath;
|
||||||
/// </summary>
|
Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
|
||||||
/// <param name="name">The name of the page.</param>
|
if (stream is null)
|
||||||
/// <response code="200">ConfigurationPage returned.</response>
|
|
||||||
/// <response code="404">Plugin configuration page not found.</response>
|
|
||||||
/// <returns>The configuration page.</returns>
|
|
||||||
[HttpGet("web/ConfigurationPage")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Html, "application/x-javascript")]
|
|
||||||
public ActionResult GetDashboardConfigurationPage([FromQuery] string? name)
|
|
||||||
{
|
{
|
||||||
var altPage = GetPluginPages().FirstOrDefault(p => string.Equals(p.Item1.Name, name, StringComparison.OrdinalIgnoreCase));
|
_logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
|
||||||
if (altPage is null)
|
return NotFound();
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
IPlugin plugin = altPage.Item2;
|
|
||||||
string resourcePath = altPage.Item1.EmbeddedResourcePath;
|
|
||||||
Stream? stream = plugin.GetType().Assembly.GetManifestResourceStream(resourcePath);
|
|
||||||
if (stream is null)
|
|
||||||
{
|
|
||||||
_logger.LogError("Failed to get resource {Resource} from plugin {Plugin}", resourcePath, plugin.Name);
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return File(stream, MimeTypes.GetMimeType(resourcePath));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
|
return File(stream, MimeTypes.GetMimeType(resourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<ConfigurationPageInfo> GetConfigPages(LocalPlugin plugin)
|
||||||
|
{
|
||||||
|
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
|
||||||
|
{
|
||||||
|
if (plugin.Instance is not IHasWebPages hasWebPages)
|
||||||
{
|
{
|
||||||
return GetPluginPages(plugin).Select(i => new ConfigurationPageInfo(plugin.Instance, i.Item1));
|
return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages(LocalPlugin plugin)
|
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
|
||||||
{
|
}
|
||||||
if (plugin.Instance is not IHasWebPages hasWebPages)
|
|
||||||
{
|
|
||||||
return Enumerable.Empty<Tuple<PluginPageInfo, IPlugin>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasWebPages.GetPages().Select(i => new Tuple<PluginPageInfo, IPlugin>(i, plugin.Instance));
|
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
|
||||||
}
|
{
|
||||||
|
return _pluginManager.Plugins.SelectMany(GetPluginPages);
|
||||||
private IEnumerable<Tuple<PluginPageInfo, IPlugin>> GetPluginPages()
|
|
||||||
{
|
|
||||||
return _pluginManager.Plugins.SelectMany(GetPluginPages);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,129 +13,128 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devices Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public class DevicesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IDeviceManager _deviceManager;
|
||||||
|
private readonly ISessionManager _sessionManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Devices Controller.
|
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
||||||
public class DevicesController : BaseJellyfinApiController
|
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
||||||
|
public DevicesController(
|
||||||
|
IDeviceManager deviceManager,
|
||||||
|
ISessionManager sessionManager)
|
||||||
{
|
{
|
||||||
private readonly IDeviceManager _deviceManager;
|
_deviceManager = deviceManager;
|
||||||
private readonly ISessionManager _sessionManager;
|
_sessionManager = sessionManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DevicesController"/> class.
|
/// Get Devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="deviceManager">Instance of <see cref="IDeviceManager"/> interface.</param>
|
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
||||||
/// <param name="sessionManager">Instance of <see cref="ISessionManager"/> interface.</param>
|
/// <param name="userId">Gets or sets the user identifier.</param>
|
||||||
public DevicesController(
|
/// <response code="200">Devices retrieved.</response>
|
||||||
IDeviceManager deviceManager,
|
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
||||||
ISessionManager sessionManager)
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
||||||
|
{
|
||||||
|
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get info for a device.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Device Id.</param>
|
||||||
|
/// <response code="200">Device info retrieved.</response>
|
||||||
|
/// <response code="404">Device not found.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||||
|
[HttpGet("Info")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
||||||
|
{
|
||||||
|
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||||
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
_deviceManager = deviceManager;
|
return NotFound();
|
||||||
_sessionManager = sessionManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return deviceInfo;
|
||||||
/// Get Devices.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
|
/// <summary>
|
||||||
/// <param name="userId">Gets or sets the user identifier.</param>
|
/// Get options for a device.
|
||||||
/// <response code="200">Devices retrieved.</response>
|
/// </summary>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
|
/// <param name="id">Device Id.</param>
|
||||||
[HttpGet]
|
/// <response code="200">Device options retrieved.</response>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="404">Device not found.</response>
|
||||||
public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId)
|
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||||
|
[HttpGet("Options")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
||||||
|
{
|
||||||
|
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
||||||
|
if (deviceInfo is null)
|
||||||
{
|
{
|
||||||
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false);
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return deviceInfo;
|
||||||
/// Get info for a device.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Device Id.</param>
|
|
||||||
/// <response code="200">Device info retrieved.</response>
|
|
||||||
/// <response code="404">Device not found.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
|
||||||
[HttpGet("Info")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult<DeviceInfo>> GetDeviceInfo([FromQuery, Required] string id)
|
|
||||||
{
|
|
||||||
var deviceInfo = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
|
||||||
if (deviceInfo is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceInfo;
|
/// <summary>
|
||||||
|
/// Update device options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Device Id.</param>
|
||||||
|
/// <param name="deviceOptions">Device Options.</param>
|
||||||
|
/// <response code="204">Device options updated.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpPost("Options")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> UpdateDeviceOptions(
|
||||||
|
[FromQuery, Required] string id,
|
||||||
|
[FromBody, Required] DeviceOptionsDto deviceOptions)
|
||||||
|
{
|
||||||
|
await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a device.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Device Id.</param>
|
||||||
|
/// <response code="204">Device deleted.</response>
|
||||||
|
/// <response code="404">Device not found.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
||||||
|
[HttpDelete]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
||||||
|
{
|
||||||
|
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
||||||
|
if (existingDevice is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
||||||
/// Get options for a device.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Device Id.</param>
|
|
||||||
/// <response code="200">Device options retrieved.</response>
|
|
||||||
/// <response code="404">Device not found.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the device info on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
|
||||||
[HttpGet("Options")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult<DeviceOptions>> GetDeviceOptions([FromQuery, Required] string id)
|
|
||||||
{
|
|
||||||
var deviceInfo = await _deviceManager.GetDeviceOptions(id).ConfigureAwait(false);
|
|
||||||
if (deviceInfo is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return deviceInfo;
|
foreach (var session in sessions.Items)
|
||||||
|
{
|
||||||
|
await _sessionManager.Logout(session).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// Update device options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Device Id.</param>
|
|
||||||
/// <param name="deviceOptions">Device Options.</param>
|
|
||||||
/// <response code="204">Device options updated.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
|
||||||
[HttpPost("Options")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> UpdateDeviceOptions(
|
|
||||||
[FromQuery, Required] string id,
|
|
||||||
[FromBody, Required] DeviceOptionsDto deviceOptions)
|
|
||||||
{
|
|
||||||
await _deviceManager.UpdateDeviceOptions(id, deviceOptions.CustomName).ConfigureAwait(false);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a device.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="id">Device Id.</param>
|
|
||||||
/// <response code="204">Device deleted.</response>
|
|
||||||
/// <response code="404">Device not found.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
|
|
||||||
[HttpDelete]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public async Task<ActionResult> DeleteDevice([FromQuery, Required] string id)
|
|
||||||
{
|
|
||||||
var existingDevice = await _deviceManager.GetDevice(id).ConfigureAwait(false);
|
|
||||||
if (existingDevice is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var sessions = await _deviceManager.GetDevices(new DeviceQuery { DeviceId = id }).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var session in sessions.Items)
|
|
||||||
{
|
|
||||||
await _sessionManager.Logout(session).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Common.Extensions;
|
using MediaBrowser.Common.Extensions;
|
||||||
@ -14,201 +13,200 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Display Preferences Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
public class DisplayPreferencesController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
||||||
|
private readonly ILogger<DisplayPreferencesController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Display Preferences Controller.
|
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
||||||
public class DisplayPreferencesController : BaseJellyfinApiController
|
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
|
||||||
|
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
|
||||||
{
|
{
|
||||||
private readonly IDisplayPreferencesManager _displayPreferencesManager;
|
_displayPreferencesManager = displayPreferencesManager;
|
||||||
private readonly ILogger<DisplayPreferencesController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DisplayPreferencesController"/> class.
|
/// Get Display Preferences.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="displayPreferencesManager">Instance of <see cref="IDisplayPreferencesManager"/> interface.</param>
|
/// <param name="displayPreferencesId">Display preferences id.</param>
|
||||||
/// <param name="logger">Instance of <see cref="ILogger{DisplayPreferencesController}"/> interface.</param>
|
/// <param name="userId">User id.</param>
|
||||||
public DisplayPreferencesController(IDisplayPreferencesManager displayPreferencesManager, ILogger<DisplayPreferencesController> logger)
|
/// <param name="client">Client.</param>
|
||||||
|
/// <response code="200">Display preferences retrieved.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
|
||||||
|
[HttpGet("{displayPreferencesId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||||
|
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
|
||||||
|
[FromRoute, Required] string displayPreferencesId,
|
||||||
|
[FromQuery, Required] Guid userId,
|
||||||
|
[FromQuery, Required] string client)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(displayPreferencesId, out var itemId))
|
||||||
{
|
{
|
||||||
_displayPreferencesManager = displayPreferencesManager;
|
itemId = displayPreferencesId.GetMD5();
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||||
/// Get Display Preferences.
|
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
||||||
/// </summary>
|
itemPreferences.ItemId = itemId;
|
||||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
|
||||||
/// <param name="userId">User id.</param>
|
var dto = new DisplayPreferencesDto
|
||||||
/// <param name="client">Client.</param>
|
|
||||||
/// <response code="200">Display preferences retrieved.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the display preferences on success, or a <see cref="NotFoundResult"/> if the display preferences could not be found.</returns>
|
|
||||||
[HttpGet("{displayPreferencesId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
|
||||||
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
|
|
||||||
[FromRoute, Required] string displayPreferencesId,
|
|
||||||
[FromQuery, Required] Guid userId,
|
|
||||||
[FromQuery, Required] string client)
|
|
||||||
{
|
{
|
||||||
if (!Guid.TryParse(displayPreferencesId, out var itemId))
|
Client = displayPreferences.Client,
|
||||||
{
|
Id = displayPreferences.ItemId.ToString(),
|
||||||
itemId = displayPreferencesId.GetMD5();
|
SortBy = itemPreferences.SortBy,
|
||||||
}
|
SortOrder = itemPreferences.SortOrder,
|
||||||
|
IndexBy = displayPreferences.IndexBy?.ToString(),
|
||||||
|
RememberIndexing = itemPreferences.RememberIndexing,
|
||||||
|
RememberSorting = itemPreferences.RememberSorting,
|
||||||
|
ScrollDirection = displayPreferences.ScrollDirection,
|
||||||
|
ShowBackdrop = displayPreferences.ShowBackdrop,
|
||||||
|
ShowSidebar = displayPreferences.ShowSidebar
|
||||||
|
};
|
||||||
|
|
||||||
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
foreach (var homeSection in displayPreferences.HomeSections)
|
||||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
{
|
||||||
itemPreferences.ItemId = itemId;
|
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
||||||
|
|
||||||
var dto = new DisplayPreferencesDto
|
|
||||||
{
|
|
||||||
Client = displayPreferences.Client,
|
|
||||||
Id = displayPreferences.ItemId.ToString(),
|
|
||||||
SortBy = itemPreferences.SortBy,
|
|
||||||
SortOrder = itemPreferences.SortOrder,
|
|
||||||
IndexBy = displayPreferences.IndexBy?.ToString(),
|
|
||||||
RememberIndexing = itemPreferences.RememberIndexing,
|
|
||||||
RememberSorting = itemPreferences.RememberSorting,
|
|
||||||
ScrollDirection = displayPreferences.ScrollDirection,
|
|
||||||
ShowBackdrop = displayPreferences.ShowBackdrop,
|
|
||||||
ShowSidebar = displayPreferences.ShowSidebar
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var homeSection in displayPreferences.HomeSections)
|
|
||||||
{
|
|
||||||
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
|
||||||
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
|
||||||
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
|
||||||
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
|
||||||
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
|
||||||
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
|
|
||||||
|
|
||||||
// Load all custom display preferences
|
|
||||||
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
|
||||||
foreach (var (key, value) in customDisplayPreferences)
|
|
||||||
{
|
|
||||||
dto.CustomPrefs.TryAdd(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
|
|
||||||
_displayPreferencesManager.SaveChanges();
|
|
||||||
|
|
||||||
return dto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
|
||||||
/// Update Display Preferences.
|
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
/// </summary>
|
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString(CultureInfo.InvariantCulture);
|
||||||
/// <param name="displayPreferencesId">Display preferences id.</param>
|
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
|
||||||
/// <param name="userId">User Id.</param>
|
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
|
||||||
/// <param name="client">Client.</param>
|
dto.CustomPrefs["dashboardTheme"] = displayPreferences.DashboardTheme;
|
||||||
/// <param name="displayPreferences">New Display Preferences object.</param>
|
|
||||||
/// <response code="204">Display preferences updated.</response>
|
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
|
||||||
[HttpPost("{displayPreferencesId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
|
||||||
public ActionResult UpdateDisplayPreferences(
|
|
||||||
[FromRoute, Required] string displayPreferencesId,
|
|
||||||
[FromQuery, Required] Guid userId,
|
|
||||||
[FromQuery, Required] string client,
|
|
||||||
[FromBody, Required] DisplayPreferencesDto displayPreferences)
|
|
||||||
{
|
|
||||||
HomeSectionType[] defaults =
|
|
||||||
{
|
|
||||||
HomeSectionType.SmallLibraryTiles,
|
|
||||||
HomeSectionType.Resume,
|
|
||||||
HomeSectionType.ResumeAudio,
|
|
||||||
HomeSectionType.ResumeBook,
|
|
||||||
HomeSectionType.LiveTv,
|
|
||||||
HomeSectionType.NextUp,
|
|
||||||
HomeSectionType.LatestMedia,
|
|
||||||
HomeSectionType.None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!Guid.TryParse(displayPreferencesId, out var itemId))
|
// Load all custom display preferences
|
||||||
|
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
||||||
|
foreach (var (key, value) in customDisplayPreferences)
|
||||||
|
{
|
||||||
|
dto.CustomPrefs.TryAdd(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
|
||||||
|
_displayPreferencesManager.SaveChanges();
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update Display Preferences.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="displayPreferencesId">Display preferences id.</param>
|
||||||
|
/// <param name="userId">User Id.</param>
|
||||||
|
/// <param name="client">Client.</param>
|
||||||
|
/// <param name="displayPreferences">New Display Preferences object.</param>
|
||||||
|
/// <response code="204">Display preferences updated.</response>
|
||||||
|
/// <returns>An <see cref="NoContentResult"/> on success.</returns>
|
||||||
|
[HttpPost("{displayPreferencesId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||||
|
public ActionResult UpdateDisplayPreferences(
|
||||||
|
[FromRoute, Required] string displayPreferencesId,
|
||||||
|
[FromQuery, Required] Guid userId,
|
||||||
|
[FromQuery, Required] string client,
|
||||||
|
[FromBody, Required] DisplayPreferencesDto displayPreferences)
|
||||||
|
{
|
||||||
|
HomeSectionType[] defaults =
|
||||||
|
{
|
||||||
|
HomeSectionType.SmallLibraryTiles,
|
||||||
|
HomeSectionType.Resume,
|
||||||
|
HomeSectionType.ResumeAudio,
|
||||||
|
HomeSectionType.ResumeBook,
|
||||||
|
HomeSectionType.LiveTv,
|
||||||
|
HomeSectionType.NextUp,
|
||||||
|
HomeSectionType.LatestMedia,
|
||||||
|
HomeSectionType.None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!Guid.TryParse(displayPreferencesId, out var itemId))
|
||||||
|
{
|
||||||
|
itemId = displayPreferencesId.GetMD5();
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||||
|
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
|
||||||
|
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
||||||
|
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
||||||
|
|
||||||
|
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
|
||||||
|
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
|
||||||
|
&& !string.IsNullOrEmpty(chromecastVersion)
|
||||||
|
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
|
||||||
|
: ChromecastVersion.Stable;
|
||||||
|
displayPreferences.CustomPrefs.Remove("chromecastVersion");
|
||||||
|
|
||||||
|
existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
|
||||||
|
|| string.IsNullOrEmpty(enableNextVideoInfoOverlay)
|
||||||
|
|| bool.Parse(enableNextVideoInfoOverlay);
|
||||||
|
displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
|
||||||
|
|
||||||
|
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
|
||||||
|
&& !string.IsNullOrEmpty(skipBackLength)
|
||||||
|
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
|
||||||
|
: 10000;
|
||||||
|
displayPreferences.CustomPrefs.Remove("skipBackLength");
|
||||||
|
|
||||||
|
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
|
||||||
|
&& !string.IsNullOrEmpty(skipForwardLength)
|
||||||
|
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
|
||||||
|
: 30000;
|
||||||
|
displayPreferences.CustomPrefs.Remove("skipForwardLength");
|
||||||
|
|
||||||
|
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
|
||||||
|
? theme
|
||||||
|
: string.Empty;
|
||||||
|
displayPreferences.CustomPrefs.Remove("dashboardTheme");
|
||||||
|
|
||||||
|
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
|
||||||
|
? home
|
||||||
|
: string.Empty;
|
||||||
|
displayPreferences.CustomPrefs.Remove("tvhome");
|
||||||
|
|
||||||
|
existingDisplayPreferences.HomeSections.Clear();
|
||||||
|
|
||||||
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture);
|
||||||
|
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||||
{
|
{
|
||||||
itemId = displayPreferencesId.GetMD5();
|
type = order < 8 ? defaults[order] : HomeSectionType.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
displayPreferences.CustomPrefs.Remove(key);
|
||||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
|
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
|
||||||
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
}
|
||||||
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
|
||||||
|
|
||||||
existingDisplayPreferences.ScrollDirection = displayPreferences.ScrollDirection;
|
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
||||||
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
|
{
|
||||||
&& !string.IsNullOrEmpty(chromecastVersion)
|
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
|
||||||
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
|
|
||||||
: ChromecastVersion.Stable;
|
|
||||||
displayPreferences.CustomPrefs.Remove("chromecastVersion");
|
|
||||||
|
|
||||||
existingDisplayPreferences.EnableNextVideoInfoOverlay = !displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
|
|
||||||
|| string.IsNullOrEmpty(enableNextVideoInfoOverlay)
|
|
||||||
|| bool.Parse(enableNextVideoInfoOverlay);
|
|
||||||
displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
|
|
||||||
|
|
||||||
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
|
|
||||||
&& !string.IsNullOrEmpty(skipBackLength)
|
|
||||||
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
|
|
||||||
: 10000;
|
|
||||||
displayPreferences.CustomPrefs.Remove("skipBackLength");
|
|
||||||
|
|
||||||
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
|
|
||||||
&& !string.IsNullOrEmpty(skipForwardLength)
|
|
||||||
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
|
|
||||||
: 30000;
|
|
||||||
displayPreferences.CustomPrefs.Remove("skipForwardLength");
|
|
||||||
|
|
||||||
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
|
|
||||||
? theme
|
|
||||||
: string.Empty;
|
|
||||||
displayPreferences.CustomPrefs.Remove("dashboardTheme");
|
|
||||||
|
|
||||||
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
|
|
||||||
? home
|
|
||||||
: string.Empty;
|
|
||||||
displayPreferences.CustomPrefs.Remove("tvhome");
|
|
||||||
|
|
||||||
existingDisplayPreferences.HomeSections.Clear();
|
|
||||||
|
|
||||||
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
{
|
||||||
var order = int.Parse(key.AsSpan().Slice("homesection".Length), CultureInfo.InvariantCulture);
|
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
||||||
if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
|
|
||||||
{
|
|
||||||
type = order < 8 ? defaults[order] : HomeSectionType.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
displayPreferences.CustomPrefs.Remove(key);
|
displayPreferences.CustomPrefs.Remove(key);
|
||||||
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
if (!Enum.TryParse<ViewType>(displayPreferences.CustomPrefs[key], true, out var type))
|
|
||||||
{
|
|
||||||
_logger.LogError("Invalid ViewType: {LandingScreenOption}", displayPreferences.CustomPrefs[key]);
|
|
||||||
displayPreferences.CustomPrefs.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
|
|
||||||
itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName";
|
|
||||||
itemPrefs.SortOrder = displayPreferences.SortOrder;
|
|
||||||
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
|
|
||||||
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
|
||||||
itemPrefs.ItemId = itemId;
|
|
||||||
|
|
||||||
// Set all remaining custom preferences.
|
|
||||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
|
||||||
_displayPreferencesManager.SaveChanges();
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
|
||||||
|
itemPrefs.SortBy = displayPreferences.SortBy ?? "SortName";
|
||||||
|
itemPrefs.SortOrder = displayPreferences.SortOrder;
|
||||||
|
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
|
||||||
|
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
|
||||||
|
itemPrefs.ItemId = itemId;
|
||||||
|
|
||||||
|
// Set all remaining custom preferences.
|
||||||
|
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||||
|
_displayPreferencesManager.SaveChanges();
|
||||||
|
|
||||||
|
return NoContent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,127 +7,126 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dlna Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public class DlnaController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dlna Controller.
|
/// Initializes a new instance of the <see cref="DlnaController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
public class DlnaController : BaseJellyfinApiController
|
public DlnaController(IDlnaManager dlnaManager)
|
||||||
{
|
{
|
||||||
private readonly IDlnaManager _dlnaManager;
|
_dlnaManager = dlnaManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DlnaController"/> class.
|
/// Get profile infos.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <response code="200">Device profile infos returned.</response>
|
||||||
public DlnaController(IDlnaManager dlnaManager)
|
/// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
|
||||||
|
[HttpGet("ProfileInfos")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
|
||||||
|
{
|
||||||
|
return Ok(_dlnaManager.GetProfileInfos());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default profile.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">Default device profile returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
|
||||||
|
[HttpGet("Profiles/Default")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<DeviceProfile> GetDefaultProfile()
|
||||||
|
{
|
||||||
|
return _dlnaManager.GetDefaultProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a single profile.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profileId">Profile Id.</param>
|
||||||
|
/// <response code="200">Device profile returned.</response>
|
||||||
|
/// <response code="404">Device profile not found.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
|
||||||
|
[HttpGet("Profiles/{profileId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
|
||||||
|
{
|
||||||
|
var profile = _dlnaManager.GetProfile(profileId);
|
||||||
|
if (profile is null)
|
||||||
{
|
{
|
||||||
_dlnaManager = dlnaManager;
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return profile;
|
||||||
/// Get profile infos.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Device profile infos returned.</response>
|
/// <summary>
|
||||||
/// <returns>An <see cref="OkResult"/> containing the device profile infos.</returns>
|
/// Deletes a profile.
|
||||||
[HttpGet("ProfileInfos")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <param name="profileId">Profile id.</param>
|
||||||
public ActionResult<IEnumerable<DeviceProfileInfo>> GetProfileInfos()
|
/// <response code="204">Device profile deleted.</response>
|
||||||
|
/// <response code="404">Device profile not found.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
||||||
|
[HttpDelete("Profiles/{profileId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult DeleteProfile([FromRoute, Required] string profileId)
|
||||||
|
{
|
||||||
|
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
||||||
|
if (existingDeviceProfile is null)
|
||||||
{
|
{
|
||||||
return Ok(_dlnaManager.GetProfileInfos());
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_dlnaManager.DeleteProfile(profileId);
|
||||||
/// Gets the default profile.
|
return NoContent();
|
||||||
/// </summary>
|
}
|
||||||
/// <response code="200">Default device profile returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the default profile.</returns>
|
/// <summary>
|
||||||
[HttpGet("Profiles/Default")]
|
/// Creates a profile.
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// </summary>
|
||||||
public ActionResult<DeviceProfile> GetDefaultProfile()
|
/// <param name="deviceProfile">Device profile.</param>
|
||||||
|
/// <response code="204">Device profile created.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||||
|
[HttpPost("Profiles")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
|
||||||
|
{
|
||||||
|
_dlnaManager.CreateProfile(deviceProfile);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a profile.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="profileId">Profile id.</param>
|
||||||
|
/// <param name="deviceProfile">Device profile.</param>
|
||||||
|
/// <response code="204">Device profile updated.</response>
|
||||||
|
/// <response code="404">Device profile not found.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
||||||
|
[HttpPost("Profiles/{profileId}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
|
||||||
|
{
|
||||||
|
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
||||||
|
if (existingDeviceProfile is null)
|
||||||
{
|
{
|
||||||
return _dlnaManager.GetDefaultProfile();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
_dlnaManager.UpdateProfile(profileId, deviceProfile);
|
||||||
/// Gets a single profile.
|
return NoContent();
|
||||||
/// </summary>
|
|
||||||
/// <param name="profileId">Profile Id.</param>
|
|
||||||
/// <response code="200">Device profile returned.</response>
|
|
||||||
/// <response code="404">Device profile not found.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the profile on success, or a <see cref="NotFoundResult"/> if device profile not found.</returns>
|
|
||||||
[HttpGet("Profiles/{profileId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult<DeviceProfile> GetProfile([FromRoute, Required] string profileId)
|
|
||||||
{
|
|
||||||
var profile = _dlnaManager.GetProfile(profileId);
|
|
||||||
if (profile is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return profile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deletes a profile.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profileId">Profile id.</param>
|
|
||||||
/// <response code="204">Device profile deleted.</response>
|
|
||||||
/// <response code="404">Device profile not found.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
|
||||||
[HttpDelete("Profiles/{profileId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult DeleteProfile([FromRoute, Required] string profileId)
|
|
||||||
{
|
|
||||||
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
|
||||||
if (existingDeviceProfile is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
_dlnaManager.DeleteProfile(profileId);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a profile.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceProfile">Device profile.</param>
|
|
||||||
/// <response code="204">Device profile created.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
|
||||||
[HttpPost("Profiles")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public ActionResult CreateProfile([FromBody] DeviceProfile deviceProfile)
|
|
||||||
{
|
|
||||||
_dlnaManager.CreateProfile(deviceProfile);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates a profile.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="profileId">Profile id.</param>
|
|
||||||
/// <param name="deviceProfile">Device profile.</param>
|
|
||||||
/// <response code="204">Device profile updated.</response>
|
|
||||||
/// <response code="404">Device profile not found.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if profile not found.</returns>
|
|
||||||
[HttpPost("Profiles/{profileId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult UpdateProfile([FromRoute, Required] string profileId, [FromBody] DeviceProfile deviceProfile)
|
|
||||||
{
|
|
||||||
var existingDeviceProfile = _dlnaManager.GetProfile(profileId);
|
|
||||||
if (existingDeviceProfile is null)
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
_dlnaManager.UpdateProfile(profileId, deviceProfile);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,311 +14,310 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dlna Server Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Dlna")]
|
||||||
|
[DlnaEnabled]
|
||||||
|
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
||||||
|
public class DlnaServerController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IDlnaManager _dlnaManager;
|
||||||
|
private readonly IContentDirectory _contentDirectory;
|
||||||
|
private readonly IConnectionManager _connectionManager;
|
||||||
|
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dlna Server Controller.
|
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Dlna")]
|
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
||||||
[DlnaEnabled]
|
public DlnaServerController(IDlnaManager dlnaManager)
|
||||||
[Authorize(Policy = Policies.AnonymousLanAccessPolicy)]
|
|
||||||
public class DlnaServerController : BaseJellyfinApiController
|
|
||||||
{
|
{
|
||||||
private readonly IDlnaManager _dlnaManager;
|
_dlnaManager = dlnaManager;
|
||||||
private readonly IContentDirectory _contentDirectory;
|
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||||
private readonly IConnectionManager _connectionManager;
|
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||||
private readonly IMediaReceiverRegistrar _mediaReceiverRegistrar;
|
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
/// Get Description Xml.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
|
/// <param name="serverId">Server UUID.</param>
|
||||||
public DlnaServerController(IDlnaManager dlnaManager)
|
/// <response code="200">Description xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
||||||
|
[HttpGet("{serverId}/description")]
|
||||||
|
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
var url = GetAbsoluteUri();
|
||||||
|
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
||||||
|
return Ok(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna content directory xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Dlna content directory returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
||||||
|
[HttpGet("{serverId}/ContentDirectory")]
|
||||||
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
|
||||||
|
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_contentDirectory.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna media receiver registrar xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
||||||
|
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets Dlna media receiver registrar xml.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Dlna media receiver registrar xml.</returns>
|
||||||
|
[HttpGet("{serverId}/ConnectionManager")]
|
||||||
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
||||||
|
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return Ok(_connectionManager.GetServiceXml());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a content directory control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/ContentDirectory/Control")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a connection manager control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/ConnectionManager/Control")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process a media receiver registrar control request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Control response.</returns>
|
||||||
|
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
||||||
|
{
|
||||||
|
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_mediaReceiverRegistrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_contentDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Processes an event subscription request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Event subscription response.</returns>
|
||||||
|
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
|
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
||||||
|
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[Produces(MediaTypeNames.Text.Xml)]
|
||||||
|
[ProducesFile(MediaTypeNames.Text.Xml)]
|
||||||
|
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
||||||
|
{
|
||||||
|
return ProcessEventRequest(_connectionManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a server icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverId">Server UUID.</param>
|
||||||
|
/// <param name="fileName">The icon filename.</param>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="404">Not Found.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
/// <returns>Icon stream.</returns>
|
||||||
|
[HttpGet("{serverId}/icons/{fileName}")]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[ProducesImageFile]
|
||||||
|
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
||||||
|
{
|
||||||
|
return GetIconInternal(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a server icon.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileName">The icon filename.</param>
|
||||||
|
/// <returns>Icon stream.</returns>
|
||||||
|
/// <response code="200">Request processed.</response>
|
||||||
|
/// <response code="404">Not Found.</response>
|
||||||
|
/// <response code="503">DLNA is disabled.</response>
|
||||||
|
[HttpGet("icons/{fileName}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[ProducesImageFile]
|
||||||
|
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
||||||
|
{
|
||||||
|
return GetIconInternal(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult GetIconInternal(string fileName)
|
||||||
|
{
|
||||||
|
var icon = _dlnaManager.GetIcon(fileName);
|
||||||
|
if (icon is null)
|
||||||
{
|
{
|
||||||
_dlnaManager = dlnaManager;
|
return NotFound();
|
||||||
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
|
||||||
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
|
||||||
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return File(icon.Stream, MimeTypes.GetMimeType(fileName));
|
||||||
/// Get Description Xml.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Description xml returned.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the description xml.</returns>
|
|
||||||
[HttpGet("{serverId}/description")]
|
|
||||||
[HttpGet("{serverId}/description.xml", Name = "GetDescriptionXml_2")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public ActionResult<string> GetDescriptionXml([FromRoute, Required] string serverId)
|
|
||||||
{
|
|
||||||
var url = GetAbsoluteUri();
|
|
||||||
var serverAddress = url.Substring(0, url.IndexOf("/dlna/", StringComparison.OrdinalIgnoreCase));
|
|
||||||
var xml = _dlnaManager.GetServerDescriptionXml(Request.Headers, serverId, serverAddress);
|
|
||||||
return Ok(xml);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
private string GetAbsoluteUri()
|
||||||
/// Gets Dlna content directory xml.
|
{
|
||||||
/// </summary>
|
return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
|
||||||
/// <param name="serverId">Server UUID.</param>
|
}
|
||||||
/// <response code="200">Dlna content directory returned.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the dlna content directory xml.</returns>
|
|
||||||
[HttpGet("{serverId}/ContentDirectory")]
|
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory", Name = "GetContentDirectory_2")]
|
|
||||||
[HttpGet("{serverId}/ContentDirectory/ContentDirectory.xml", Name = "GetContentDirectory_3")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
public ActionResult<string> GetContentDirectory([FromRoute, Required] string serverId)
|
|
||||||
{
|
|
||||||
return Ok(_contentDirectory.GetServiceXml());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
||||||
/// Gets Dlna media receiver registrar xml.
|
{
|
||||||
/// </summary>
|
return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar")]
|
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar", Name = "GetMediaReceiverRegistrar_2")]
|
|
||||||
[HttpGet("{serverId}/MediaReceiverRegistrar/MediaReceiverRegistrar.xml", Name = "GetMediaReceiverRegistrar_3")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
public ActionResult<string> GetMediaReceiverRegistrar([FromRoute, Required] string serverId)
|
|
||||||
{
|
{
|
||||||
return Ok(_mediaReceiverRegistrar.GetServiceXml());
|
InputXml = requestStream,
|
||||||
}
|
TargetServerUuId = id,
|
||||||
|
RequestedUrl = GetAbsoluteUri()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
|
||||||
/// Gets Dlna media receiver registrar xml.
|
{
|
||||||
/// </summary>
|
var subscriptionId = Request.Headers["SID"];
|
||||||
/// <param name="serverId">Server UUID.</param>
|
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||||
/// <response code="200">Dlna media receiver registrar xml returned.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Dlna media receiver registrar xml.</returns>
|
|
||||||
[HttpGet("{serverId}/ConnectionManager")]
|
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager", Name = "GetConnectionManager_2")]
|
|
||||||
[HttpGet("{serverId}/ConnectionManager/ConnectionManager.xml", Name = "GetConnectionManager_3")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
public ActionResult<string> GetConnectionManager([FromRoute, Required] string serverId)
|
|
||||||
{
|
{
|
||||||
return Ok(_connectionManager.GetServiceXml());
|
var notificationType = Request.Headers["NT"];
|
||||||
}
|
var callback = Request.Headers["CALLBACK"];
|
||||||
|
var timeoutString = Request.Headers["TIMEOUT"];
|
||||||
|
|
||||||
/// <summary>
|
if (string.IsNullOrEmpty(notificationType))
|
||||||
/// Process a content directory control request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Control response.</returns>
|
|
||||||
[HttpPost("{serverId}/ContentDirectory/Control")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessContentDirectoryControlRequest([FromRoute, Required] string serverId)
|
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _contentDirectory).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a connection manager control request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Control response.</returns>
|
|
||||||
[HttpPost("{serverId}/ConnectionManager/Control")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessConnectionManagerControlRequest([FromRoute, Required] string serverId)
|
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _connectionManager).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Process a media receiver registrar control request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Control response.</returns>
|
|
||||||
[HttpPost("{serverId}/MediaReceiverRegistrar/Control")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public async Task<ActionResult<ControlResponse>> ProcessMediaReceiverRegistrarControlRequest([FromRoute, Required] string serverId)
|
|
||||||
{
|
|
||||||
return await ProcessControlRequestInternalAsync(serverId, Request.Body, _mediaReceiverRegistrar).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes an event subscription request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Event subscription response.</returns>
|
|
||||||
[HttpSubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
|
||||||
[HttpUnsubscribe("{serverId}/MediaReceiverRegistrar/Events")]
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessMediaReceiverRegistrarEventRequest(string serverId)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(_mediaReceiverRegistrar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes an event subscription request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Event subscription response.</returns>
|
|
||||||
[HttpSubscribe("{serverId}/ContentDirectory/Events")]
|
|
||||||
[HttpUnsubscribe("{serverId}/ContentDirectory/Events")]
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessContentDirectoryEventRequest(string serverId)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(_contentDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Processes an event subscription request.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Event subscription response.</returns>
|
|
||||||
[HttpSubscribe("{serverId}/ConnectionManager/Events")]
|
|
||||||
[HttpUnsubscribe("{serverId}/ConnectionManager/Events")]
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)] // Ignore in openapi docs
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[Produces(MediaTypeNames.Text.Xml)]
|
|
||||||
[ProducesFile(MediaTypeNames.Text.Xml)]
|
|
||||||
public ActionResult<EventSubscriptionResponse> ProcessConnectionManagerEventRequest(string serverId)
|
|
||||||
{
|
|
||||||
return ProcessEventRequest(_connectionManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a server icon.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serverId">Server UUID.</param>
|
|
||||||
/// <param name="fileName">The icon filename.</param>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="404">Not Found.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
/// <returns>Icon stream.</returns>
|
|
||||||
[HttpGet("{serverId}/icons/{fileName}")]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "serverId", Justification = "Required for DLNA")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[ProducesImageFile]
|
|
||||||
public ActionResult GetIconId([FromRoute, Required] string serverId, [FromRoute, Required] string fileName)
|
|
||||||
{
|
|
||||||
return GetIconInternal(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a server icon.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileName">The icon filename.</param>
|
|
||||||
/// <returns>Icon stream.</returns>
|
|
||||||
/// <response code="200">Request processed.</response>
|
|
||||||
/// <response code="404">Not Found.</response>
|
|
||||||
/// <response code="503">DLNA is disabled.</response>
|
|
||||||
[HttpGet("icons/{fileName}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
|
|
||||||
[ProducesImageFile]
|
|
||||||
public ActionResult GetIcon([FromRoute, Required] string fileName)
|
|
||||||
{
|
|
||||||
return GetIconInternal(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ActionResult GetIconInternal(string fileName)
|
|
||||||
{
|
|
||||||
var icon = _dlnaManager.GetIcon(fileName);
|
|
||||||
if (icon is null)
|
|
||||||
{
|
{
|
||||||
return NotFound();
|
return dlnaEventManager.RenewEventSubscription(
|
||||||
|
subscriptionId,
|
||||||
|
notificationType,
|
||||||
|
timeoutString,
|
||||||
|
callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
return File(icon.Stream, MimeTypes.GetMimeType(fileName));
|
return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetAbsoluteUri()
|
return dlnaEventManager.CancelEventSubscription(subscriptionId);
|
||||||
{
|
|
||||||
return $"{Request.Scheme}://{Request.Host}{Request.PathBase}{Request.Path}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<ControlResponse> ProcessControlRequestInternalAsync(string id, Stream requestStream, IUpnpService service)
|
|
||||||
{
|
|
||||||
return service.ProcessControlRequestAsync(new ControlRequest(Request.Headers)
|
|
||||||
{
|
|
||||||
InputXml = requestStream,
|
|
||||||
TargetServerUuId = id,
|
|
||||||
RequestedUrl = GetAbsoluteUri()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventSubscriptionResponse ProcessEventRequest(IDlnaEventManager dlnaEventManager)
|
|
||||||
{
|
|
||||||
var subscriptionId = Request.Headers["SID"];
|
|
||||||
if (string.Equals(Request.Method, "subscribe", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
var notificationType = Request.Headers["NT"];
|
|
||||||
var callback = Request.Headers["CALLBACK"];
|
|
||||||
var timeoutString = Request.Headers["TIMEOUT"];
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(notificationType))
|
|
||||||
{
|
|
||||||
return dlnaEventManager.RenewEventSubscription(
|
|
||||||
subscriptionId,
|
|
||||||
notificationType,
|
|
||||||
timeoutString,
|
|
||||||
callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlnaEventManager.CreateEventSubscription(notificationType, timeoutString, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dlnaEventManager.CancelEventSubscription(subscriptionId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -12,186 +12,185 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Environment Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
||||||
|
public class EnvironmentController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private const char UncSeparator = '\\';
|
||||||
|
private const string UncStartPrefix = @"\\";
|
||||||
|
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILogger<EnvironmentController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Environment Controller.
|
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class EnvironmentController : BaseJellyfinApiController
|
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
|
||||||
|
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
|
||||||
{
|
{
|
||||||
private const char UncSeparator = '\\';
|
_fileSystem = fileSystem;
|
||||||
private const string UncStartPrefix = @"\\";
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly IFileSystem _fileSystem;
|
/// <summary>
|
||||||
private readonly ILogger<EnvironmentController> _logger;
|
/// Gets the contents of a given directory in the file system.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="path">The path.</param>
|
||||||
/// Initializes a new instance of the <see cref="EnvironmentController"/> class.
|
/// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
|
||||||
/// </summary>
|
/// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <response code="200">Directory contents returned.</response>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{EnvironmentController}"/> interface.</param>
|
/// <returns>Directory contents.</returns>
|
||||||
public EnvironmentController(IFileSystem fileSystem, ILogger<EnvironmentController> logger)
|
[HttpGet("DirectoryContents")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
|
||||||
|
[FromQuery, Required] string path,
|
||||||
|
[FromQuery] bool includeFiles = false,
|
||||||
|
[FromQuery] bool includeDirectories = false)
|
||||||
|
{
|
||||||
|
if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& path.LastIndexOf(UncSeparator) == 1)
|
||||||
{
|
{
|
||||||
_fileSystem = fileSystem;
|
return Array.Empty<FileSystemEntryInfo>();
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var entries =
|
||||||
/// Gets the contents of a given directory in the file system.
|
_fileSystem.GetFileSystemEntries(path)
|
||||||
/// </summary>
|
.Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
|
||||||
/// <param name="path">The path.</param>
|
.OrderBy(i => i.FullName);
|
||||||
/// <param name="includeFiles">An optional filter to include or exclude files from the results. true/false.</param>
|
|
||||||
/// <param name="includeDirectories">An optional filter to include or exclude folders from the results. true/false.</param>
|
return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
|
||||||
/// <response code="200">Directory contents returned.</response>
|
}
|
||||||
/// <returns>Directory contents.</returns>
|
|
||||||
[HttpGet("DirectoryContents")]
|
/// <summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// Validates path.
|
||||||
public IEnumerable<FileSystemEntryInfo> GetDirectoryContents(
|
/// </summary>
|
||||||
[FromQuery, Required] string path,
|
/// <param name="validatePathDto">Validate request object.</param>
|
||||||
[FromQuery] bool includeFiles = false,
|
/// <response code="204">Path validated.</response>
|
||||||
[FromQuery] bool includeDirectories = false)
|
/// <response code="404">Path not found.</response>
|
||||||
|
/// <returns>Validation status.</returns>
|
||||||
|
[HttpPost("ValidatePath")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
|
||||||
|
{
|
||||||
|
if (validatePathDto.IsFile.HasValue)
|
||||||
{
|
{
|
||||||
if (path.StartsWith(UncStartPrefix, StringComparison.OrdinalIgnoreCase)
|
if (validatePathDto.IsFile.Value)
|
||||||
&& path.LastIndexOf(UncSeparator) == 1)
|
|
||||||
{
|
{
|
||||||
return Array.Empty<FileSystemEntryInfo>();
|
if (!System.IO.File.Exists(validatePathDto.Path))
|
||||||
}
|
|
||||||
|
|
||||||
var entries =
|
|
||||||
_fileSystem.GetFileSystemEntries(path)
|
|
||||||
.Where(i => (i.IsDirectory && includeDirectories) || (!i.IsDirectory && includeFiles))
|
|
||||||
.OrderBy(i => i.FullName);
|
|
||||||
|
|
||||||
return entries.Select(f => new FileSystemEntryInfo(f.Name, f.FullName, f.IsDirectory ? FileSystemEntryType.Directory : FileSystemEntryType.File));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="validatePathDto">Validate request object.</param>
|
|
||||||
/// <response code="204">Path validated.</response>
|
|
||||||
/// <response code="404">Path not found.</response>
|
|
||||||
/// <returns>Validation status.</returns>
|
|
||||||
[HttpPost("ValidatePath")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult ValidatePath([FromBody, Required] ValidatePathDto validatePathDto)
|
|
||||||
{
|
|
||||||
if (validatePathDto.IsFile.HasValue)
|
|
||||||
{
|
|
||||||
if (validatePathDto.IsFile.Value)
|
|
||||||
{
|
{
|
||||||
if (!System.IO.File.Exists(validatePathDto.Path))
|
return NotFound();
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(validatePathDto.Path))
|
|
||||||
{
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
|
if (!Directory.Exists(validatePathDto.Path))
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validatePathDto.ValidateWritable)
|
|
||||||
{
|
|
||||||
if (validatePathDto.Path is null)
|
|
||||||
{
|
|
||||||
throw new ResourceNotFoundException(nameof(validatePathDto.Path));
|
|
||||||
}
|
|
||||||
|
|
||||||
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.IO.File.WriteAllText(file, string.Empty);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(file))
|
|
||||||
{
|
|
||||||
System.IO.File.Delete(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
/// <summary>
|
|
||||||
/// Gets network paths.
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Empty array returned.</response>
|
|
||||||
/// <returns>List of entries.</returns>
|
|
||||||
[Obsolete("This endpoint is obsolete.")]
|
|
||||||
[HttpGet("NetworkShares")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
|
if (!System.IO.File.Exists(validatePathDto.Path) && !Directory.Exists(validatePathDto.Path))
|
||||||
return Array.Empty<FileSystemEntryInfo>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets available drives from the server's file system.
|
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">List of entries returned.</response>
|
|
||||||
/// <returns>List of entries.</returns>
|
|
||||||
[HttpGet("Drives")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public IEnumerable<FileSystemEntryInfo> GetDrives()
|
|
||||||
{
|
|
||||||
return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the parent path of a given path.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The path.</param>
|
|
||||||
/// <returns>Parent path.</returns>
|
|
||||||
[HttpGet("ParentPath")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
|
|
||||||
{
|
|
||||||
string? parent = Path.GetDirectoryName(path);
|
|
||||||
if (string.IsNullOrEmpty(parent))
|
|
||||||
{
|
{
|
||||||
// Check if unc share
|
return NotFound();
|
||||||
var index = path.LastIndexOf(UncSeparator);
|
}
|
||||||
|
|
||||||
if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
|
if (validatePathDto.ValidateWritable)
|
||||||
|
{
|
||||||
|
if (validatePathDto.Path is null)
|
||||||
{
|
{
|
||||||
parent = path.Substring(0, index);
|
throw new ResourceNotFoundException(nameof(validatePathDto.Path));
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
|
var file = Path.Combine(validatePathDto.Path, Guid.NewGuid().ToString());
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.File.WriteAllText(file, string.Empty);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(file))
|
||||||
{
|
{
|
||||||
parent = null;
|
System.IO.File.Delete(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return NoContent();
|
||||||
/// Get Default directory browser.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <response code="200">Default directory browser returned.</response>
|
/// <summary>
|
||||||
/// <returns>Default directory browser.</returns>
|
/// Gets network paths.
|
||||||
[HttpGet("DefaultDirectoryBrowser")]
|
/// </summary>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// <response code="200">Empty array returned.</response>
|
||||||
public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
|
/// <returns>List of entries.</returns>
|
||||||
|
[Obsolete("This endpoint is obsolete.")]
|
||||||
|
[HttpGet("NetworkShares")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<IEnumerable<FileSystemEntryInfo>> GetNetworkShares()
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Obsolete endpoint accessed: /Environment/NetworkShares");
|
||||||
|
return Array.Empty<FileSystemEntryInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets available drives from the server's file system.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">List of entries returned.</response>
|
||||||
|
/// <returns>List of entries.</returns>
|
||||||
|
[HttpGet("Drives")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public IEnumerable<FileSystemEntryInfo> GetDrives()
|
||||||
|
{
|
||||||
|
return _fileSystem.GetDrives().Select(d => new FileSystemEntryInfo(d.Name, d.FullName, FileSystemEntryType.Directory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the parent path of a given path.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">The path.</param>
|
||||||
|
/// <returns>Parent path.</returns>
|
||||||
|
[HttpGet("ParentPath")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<string?> GetParentPath([FromQuery, Required] string path)
|
||||||
|
{
|
||||||
|
string? parent = Path.GetDirectoryName(path);
|
||||||
|
if (string.IsNullOrEmpty(parent))
|
||||||
{
|
{
|
||||||
return new DefaultDirectoryBrowserInfoDto();
|
// Check if unc share
|
||||||
|
var index = path.LastIndexOf(UncSeparator);
|
||||||
|
|
||||||
|
if (index != -1 && path.IndexOf(UncSeparator, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
|
{
|
||||||
|
parent = path.Substring(0, index);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(parent.TrimStart(UncSeparator)))
|
||||||
|
{
|
||||||
|
parent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get Default directory browser.
|
||||||
|
/// </summary>
|
||||||
|
/// <response code="200">Default directory browser returned.</response>
|
||||||
|
/// <returns>Default directory browser.</returns>
|
||||||
|
[HttpGet("DefaultDirectoryBrowser")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<DefaultDirectoryBrowserInfoDto> GetDefaultDirectoryBrowser()
|
||||||
|
{
|
||||||
|
return new DefaultDirectoryBrowserInfoDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
@ -12,205 +11,204 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Filters controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
[Authorize]
|
||||||
|
public class FilterController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Filters controller.
|
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class FilterController : BaseJellyfinApiController
|
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="FilterController"/> class.
|
/// Gets legacy query filters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="userId">Optional. User id.</param>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="parentId">Optional. Parent id.</param>
|
||||||
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
|
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
|
||||||
|
/// <response code="200">Legacy filters retrieved.</response>
|
||||||
|
/// <returns>Legacy query filters.</returns>
|
||||||
|
[HttpGet("Items/Filters")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
|
||||||
|
{
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
BaseItem? item = null;
|
||||||
|
if (includeItemTypes.Length != 1
|
||||||
|
|| !(includeItemTypes[0] == BaseItemKind.BoxSet
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Playlist
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Trailer
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Program))
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
item = _libraryManager.GetParentItem(parentId, user?.Id);
|
||||||
_userManager = userManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var query = new InternalItemsQuery
|
||||||
/// Gets legacy query filters.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">Optional. User id.</param>
|
|
||||||
/// <param name="parentId">Optional. Parent id.</param>
|
|
||||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
|
|
||||||
/// <response code="200">Legacy filters retrieved.</response>
|
|
||||||
/// <returns>Legacy query filters.</returns>
|
|
||||||
[HttpGet("Items/Filters")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] mediaTypes)
|
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
User = user,
|
||||||
? null
|
MediaTypes = mediaTypes,
|
||||||
: _userManager.GetUserById(userId.Value);
|
IncludeItemTypes = includeItemTypes,
|
||||||
|
Recursive = true,
|
||||||
BaseItem? item = null;
|
EnableTotalRecordCount = false,
|
||||||
if (includeItemTypes.Length != 1
|
DtoOptions = new DtoOptions
|
||||||
|| !(includeItemTypes[0] == BaseItemKind.BoxSet
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Playlist
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Trailer
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Program))
|
|
||||||
{
|
{
|
||||||
item = _libraryManager.GetParentItem(parentId, user?.Id);
|
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
|
||||||
|
EnableImages = false,
|
||||||
|
EnableUserData = false
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var query = new InternalItemsQuery
|
if (item is not Folder folder)
|
||||||
{
|
{
|
||||||
User = user,
|
return new QueryFiltersLegacy();
|
||||||
MediaTypes = mediaTypes,
|
|
||||||
IncludeItemTypes = includeItemTypes,
|
|
||||||
Recursive = true,
|
|
||||||
EnableTotalRecordCount = false,
|
|
||||||
DtoOptions = new DtoOptions
|
|
||||||
{
|
|
||||||
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
|
|
||||||
EnableImages = false,
|
|
||||||
EnableUserData = false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (item is not Folder folder)
|
|
||||||
{
|
|
||||||
return new QueryFiltersLegacy();
|
|
||||||
}
|
|
||||||
|
|
||||||
var itemList = folder.GetItemList(query);
|
|
||||||
return new QueryFiltersLegacy
|
|
||||||
{
|
|
||||||
Years = itemList.Select(i => i.ProductionYear ?? -1)
|
|
||||||
.Where(i => i > 0)
|
|
||||||
.Distinct()
|
|
||||||
.Order()
|
|
||||||
.ToArray(),
|
|
||||||
|
|
||||||
Genres = itemList.SelectMany(i => i.Genres)
|
|
||||||
.DistinctNames()
|
|
||||||
.Order()
|
|
||||||
.ToArray(),
|
|
||||||
|
|
||||||
Tags = itemList
|
|
||||||
.SelectMany(i => i.Tags)
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Order()
|
|
||||||
.ToArray(),
|
|
||||||
|
|
||||||
OfficialRatings = itemList
|
|
||||||
.Select(i => i.OfficialRating)
|
|
||||||
.Where(i => !string.IsNullOrWhiteSpace(i))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Order()
|
|
||||||
.ToArray()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var itemList = folder.GetItemList(query);
|
||||||
/// Gets query filters.
|
return new QueryFiltersLegacy
|
||||||
/// </summary>
|
|
||||||
/// <param name="userId">Optional. User id.</param>
|
|
||||||
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
|
||||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="isAiring">Optional. Is item airing.</param>
|
|
||||||
/// <param name="isMovie">Optional. Is item movie.</param>
|
|
||||||
/// <param name="isSports">Optional. Is item sports.</param>
|
|
||||||
/// <param name="isKids">Optional. Is item kids.</param>
|
|
||||||
/// <param name="isNews">Optional. Is item news.</param>
|
|
||||||
/// <param name="isSeries">Optional. Is item series.</param>
|
|
||||||
/// <param name="recursive">Optional. Search recursive.</param>
|
|
||||||
/// <response code="200">Filters retrieved.</response>
|
|
||||||
/// <returns>Query filters.</returns>
|
|
||||||
[HttpGet("Items/Filters2")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryFilters> GetQueryFilters(
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
|
||||||
[FromQuery] bool? isAiring,
|
|
||||||
[FromQuery] bool? isMovie,
|
|
||||||
[FromQuery] bool? isSports,
|
|
||||||
[FromQuery] bool? isKids,
|
|
||||||
[FromQuery] bool? isNews,
|
|
||||||
[FromQuery] bool? isSeries,
|
|
||||||
[FromQuery] bool? recursive)
|
|
||||||
{
|
{
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
Years = itemList.Select(i => i.ProductionYear ?? -1)
|
||||||
? null
|
.Where(i => i > 0)
|
||||||
: _userManager.GetUserById(userId.Value);
|
.Distinct()
|
||||||
|
.Order()
|
||||||
|
.ToArray(),
|
||||||
|
|
||||||
BaseItem? parentItem = null;
|
Genres = itemList.SelectMany(i => i.Genres)
|
||||||
if (includeItemTypes.Length == 1
|
.DistinctNames()
|
||||||
&& (includeItemTypes[0] == BaseItemKind.BoxSet
|
.Order()
|
||||||
|| includeItemTypes[0] == BaseItemKind.Playlist
|
.ToArray(),
|
||||||
|| includeItemTypes[0] == BaseItemKind.Trailer
|
|
||||||
|| includeItemTypes[0] == BaseItemKind.Program))
|
|
||||||
{
|
|
||||||
parentItem = null;
|
|
||||||
}
|
|
||||||
else if (parentId.HasValue)
|
|
||||||
{
|
|
||||||
parentItem = _libraryManager.GetItemById(parentId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var filters = new QueryFilters();
|
Tags = itemList
|
||||||
var genreQuery = new InternalItemsQuery(user)
|
.SelectMany(i => i.Tags)
|
||||||
{
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
IncludeItemTypes = includeItemTypes,
|
.Order()
|
||||||
DtoOptions = new DtoOptions
|
.ToArray(),
|
||||||
{
|
|
||||||
Fields = Array.Empty<ItemFields>(),
|
|
||||||
EnableImages = false,
|
|
||||||
EnableUserData = false
|
|
||||||
},
|
|
||||||
IsAiring = isAiring,
|
|
||||||
IsMovie = isMovie,
|
|
||||||
IsSports = isSports,
|
|
||||||
IsKids = isKids,
|
|
||||||
IsNews = isNews,
|
|
||||||
IsSeries = isSeries
|
|
||||||
};
|
|
||||||
|
|
||||||
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
|
OfficialRatings = itemList
|
||||||
{
|
.Select(i => i.OfficialRating)
|
||||||
genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
|
.Where(i => !string.IsNullOrWhiteSpace(i))
|
||||||
}
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
else
|
.Order()
|
||||||
{
|
.ToArray()
|
||||||
genreQuery.Parent = parentItem;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includeItemTypes.Length == 1
|
/// <summary>
|
||||||
&& (includeItemTypes[0] == BaseItemKind.MusicAlbum
|
/// Gets query filters.
|
||||||
|| includeItemTypes[0] == BaseItemKind.MusicVideo
|
/// </summary>
|
||||||
|| includeItemTypes[0] == BaseItemKind.MusicArtist
|
/// <param name="userId">Optional. User id.</param>
|
||||||
|| includeItemTypes[0] == BaseItemKind.Audio))
|
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
{
|
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
|
||||||
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
/// <param name="isAiring">Optional. Is item airing.</param>
|
||||||
{
|
/// <param name="isMovie">Optional. Is item movie.</param>
|
||||||
Name = i.Item.Name,
|
/// <param name="isSports">Optional. Is item sports.</param>
|
||||||
Id = i.Item.Id
|
/// <param name="isKids">Optional. Is item kids.</param>
|
||||||
}).ToArray();
|
/// <param name="isNews">Optional. Is item news.</param>
|
||||||
}
|
/// <param name="isSeries">Optional. Is item series.</param>
|
||||||
else
|
/// <param name="recursive">Optional. Search recursive.</param>
|
||||||
{
|
/// <response code="200">Filters retrieved.</response>
|
||||||
filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
|
/// <returns>Query filters.</returns>
|
||||||
{
|
[HttpGet("Items/Filters2")]
|
||||||
Name = i.Item.Name,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
Id = i.Item.Id
|
public ActionResult<QueryFilters> GetQueryFilters(
|
||||||
}).ToArray();
|
[FromQuery] Guid? userId,
|
||||||
}
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||||
|
[FromQuery] bool? isAiring,
|
||||||
|
[FromQuery] bool? isMovie,
|
||||||
|
[FromQuery] bool? isSports,
|
||||||
|
[FromQuery] bool? isKids,
|
||||||
|
[FromQuery] bool? isNews,
|
||||||
|
[FromQuery] bool? isSeries,
|
||||||
|
[FromQuery] bool? recursive)
|
||||||
|
{
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
return filters;
|
BaseItem? parentItem = null;
|
||||||
|
if (includeItemTypes.Length == 1
|
||||||
|
&& (includeItemTypes[0] == BaseItemKind.BoxSet
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Playlist
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Trailer
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Program))
|
||||||
|
{
|
||||||
|
parentItem = null;
|
||||||
}
|
}
|
||||||
|
else if (parentId.HasValue)
|
||||||
|
{
|
||||||
|
parentItem = _libraryManager.GetItemById(parentId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var filters = new QueryFilters();
|
||||||
|
var genreQuery = new InternalItemsQuery(user)
|
||||||
|
{
|
||||||
|
IncludeItemTypes = includeItemTypes,
|
||||||
|
DtoOptions = new DtoOptions
|
||||||
|
{
|
||||||
|
Fields = Array.Empty<ItemFields>(),
|
||||||
|
EnableImages = false,
|
||||||
|
EnableUserData = false
|
||||||
|
},
|
||||||
|
IsAiring = isAiring,
|
||||||
|
IsMovie = isMovie,
|
||||||
|
IsSports = isSports,
|
||||||
|
IsKids = isKids,
|
||||||
|
IsNews = isNews,
|
||||||
|
IsSeries = isSeries
|
||||||
|
};
|
||||||
|
|
||||||
|
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
|
||||||
|
{
|
||||||
|
genreQuery.AncestorIds = parentItem is null ? Array.Empty<Guid>() : new[] { parentItem.Id };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
genreQuery.Parent = parentItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeItemTypes.Length == 1
|
||||||
|
&& (includeItemTypes[0] == BaseItemKind.MusicAlbum
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.MusicVideo
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.MusicArtist
|
||||||
|
|| includeItemTypes[0] == BaseItemKind.Audio))
|
||||||
|
{
|
||||||
|
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||||
|
{
|
||||||
|
Name = i.Item.Name,
|
||||||
|
Id = i.Item.Id
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
|
||||||
|
{
|
||||||
|
Name = i.Item.Name,
|
||||||
|
Id = i.Item.Id
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
@ -18,194 +17,193 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Genre = MediaBrowser.Controller.Entities.Genre;
|
using Genre = MediaBrowser.Controller.Entities.Genre;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The genres controller.
|
||||||
|
/// </summary>
|
||||||
|
[Authorize]
|
||||||
|
public class GenresController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IDtoService _dtoService;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The genres controller.
|
/// Initializes a new instance of the <see cref="GenresController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
public class GenresController : BaseJellyfinApiController
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
|
public GenresController(
|
||||||
|
IUserManager userManager,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IDtoService dtoService)
|
||||||
{
|
{
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="GenresController"/> class.
|
/// Gets all genres from a given item, folder, or the entire library.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="searchTerm">The search term.</param>
|
||||||
public GenresController(
|
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
||||||
IUserManager userManager,
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
||||||
IDtoService dtoService)
|
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
||||||
|
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
|
||||||
|
/// <param name="userId">User id.</param>
|
||||||
|
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
||||||
|
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
||||||
|
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
||||||
|
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
||||||
|
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
||||||
|
/// <param name="enableImages">Optional, include image information in output.</param>
|
||||||
|
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
||||||
|
/// <response code="200">Genres returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
|
||||||
|
[FromQuery] int? startIndex,
|
||||||
|
[FromQuery] int? limit,
|
||||||
|
[FromQuery] string? searchTerm,
|
||||||
|
[FromQuery] Guid? parentId,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
||||||
|
[FromQuery] bool? isFavorite,
|
||||||
|
[FromQuery] int? imageTypeLimit,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
||||||
|
[FromQuery] Guid? userId,
|
||||||
|
[FromQuery] string? nameStartsWithOrGreater,
|
||||||
|
[FromQuery] string? nameStartsWith,
|
||||||
|
[FromQuery] string? nameLessThan,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
||||||
|
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
||||||
|
[FromQuery] bool? enableImages = true,
|
||||||
|
[FromQuery] bool enableTotalRecordCount = true)
|
||||||
|
{
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
||||||
|
|
||||||
|
User? user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
||||||
|
|
||||||
|
var query = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
ExcludeItemTypes = excludeItemTypes,
|
||||||
_libraryManager = libraryManager;
|
IncludeItemTypes = includeItemTypes,
|
||||||
_dtoService = dtoService;
|
StartIndex = startIndex,
|
||||||
|
Limit = limit,
|
||||||
|
IsFavorite = isFavorite,
|
||||||
|
NameLessThan = nameLessThan,
|
||||||
|
NameStartsWith = nameStartsWith,
|
||||||
|
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
||||||
|
DtoOptions = dtoOptions,
|
||||||
|
SearchTerm = searchTerm,
|
||||||
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
|
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (parentId.HasValue)
|
||||||
|
{
|
||||||
|
if (parentItem is Folder)
|
||||||
|
{
|
||||||
|
query.AncestorIds = new[] { parentId.Value };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
query.ItemIds = new[] { parentId.Value };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
QueryResult<(BaseItem, ItemCounts)> result;
|
||||||
/// Gets all genres from a given item, folder, or the entire library.
|
if (parentItem is ICollectionFolder parentCollectionFolder
|
||||||
/// </summary>
|
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|
||||||
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
|
|
||||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
|
||||||
/// <param name="searchTerm">The search term.</param>
|
|
||||||
/// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
|
|
||||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
|
||||||
/// <param name="excludeItemTypes">Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="includeItemTypes">Optional. If specified, results will be filtered in based on item type. This allows multiple, comma delimited.</param>
|
|
||||||
/// <param name="isFavorite">Optional filter by items that are marked as favorite, or not.</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>
|
|
||||||
/// <param name="userId">User id.</param>
|
|
||||||
/// <param name="nameStartsWithOrGreater">Optional filter by items whose name is sorted equally or greater than a given input string.</param>
|
|
||||||
/// <param name="nameStartsWith">Optional filter by items whose name is sorted equally than a given input string.</param>
|
|
||||||
/// <param name="nameLessThan">Optional filter by items whose name is equally or lesser than a given input string.</param>
|
|
||||||
/// <param name="sortBy">Optional. Specify one or more sort orders, comma delimited.</param>
|
|
||||||
/// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
|
|
||||||
/// <param name="enableImages">Optional, include image information in output.</param>
|
|
||||||
/// <param name="enableTotalRecordCount">Optional. Include total record count.</param>
|
|
||||||
/// <response code="200">Genres returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the queryresult of genres.</returns>
|
|
||||||
[HttpGet]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetGenres(
|
|
||||||
[FromQuery] int? startIndex,
|
|
||||||
[FromQuery] int? limit,
|
|
||||||
[FromQuery] string? searchTerm,
|
|
||||||
[FromQuery] Guid? parentId,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] excludeItemTypes,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] BaseItemKind[] includeItemTypes,
|
|
||||||
[FromQuery] bool? isFavorite,
|
|
||||||
[FromQuery] int? imageTypeLimit,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes,
|
|
||||||
[FromQuery] Guid? userId,
|
|
||||||
[FromQuery] string? nameStartsWithOrGreater,
|
|
||||||
[FromQuery] string? nameStartsWith,
|
|
||||||
[FromQuery] string? nameLessThan,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] sortBy,
|
|
||||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] SortOrder[] sortOrder,
|
|
||||||
[FromQuery] bool? enableImages = true,
|
|
||||||
[FromQuery] bool enableTotalRecordCount = true)
|
|
||||||
{
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
|
|
||||||
|
|
||||||
User? user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
|
|
||||||
var parentItem = _libraryManager.GetParentItem(parentId, userId);
|
|
||||||
|
|
||||||
var query = new InternalItemsQuery(user)
|
|
||||||
{
|
|
||||||
ExcludeItemTypes = excludeItemTypes,
|
|
||||||
IncludeItemTypes = includeItemTypes,
|
|
||||||
StartIndex = startIndex,
|
|
||||||
Limit = limit,
|
|
||||||
IsFavorite = isFavorite,
|
|
||||||
NameLessThan = nameLessThan,
|
|
||||||
NameStartsWith = nameStartsWith,
|
|
||||||
NameStartsWithOrGreater = nameStartsWithOrGreater,
|
|
||||||
DtoOptions = dtoOptions,
|
|
||||||
SearchTerm = searchTerm,
|
|
||||||
EnableTotalRecordCount = enableTotalRecordCount,
|
|
||||||
OrderBy = RequestHelpers.GetOrderBy(sortBy, sortOrder)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (parentId.HasValue)
|
|
||||||
{
|
|
||||||
if (parentItem is Folder)
|
|
||||||
{
|
|
||||||
query.AncestorIds = new[] { parentId.Value };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
query.ItemIds = new[] { parentId.Value };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryResult<(BaseItem, ItemCounts)> result;
|
|
||||||
if (parentItem is ICollectionFolder parentCollectionFolder
|
|
||||||
&& (string.Equals(parentCollectionFolder.CollectionType, CollectionType.Music, StringComparison.Ordinal)
|
|
||||||
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
|
|| string.Equals(parentCollectionFolder.CollectionType, CollectionType.MusicVideos, StringComparison.Ordinal)))
|
||||||
{
|
|
||||||
result = _libraryManager.GetMusicGenres(query);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = _libraryManager.GetGenres(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
|
||||||
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a genre, by name.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="genreName">The genre name.</param>
|
|
||||||
/// <param name="userId">The user id.</param>
|
|
||||||
/// <response code="200">Genres returned.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> containing the genre.</returns>
|
|
||||||
[HttpGet("{genreName}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
|
||||||
{
|
{
|
||||||
var dtoOptions = new DtoOptions()
|
result = _libraryManager.GetMusicGenres(query);
|
||||||
.AddClientFields(User);
|
|
||||||
|
|
||||||
Genre? item;
|
|
||||||
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
item = _libraryManager.GetGenre(genreName);
|
|
||||||
}
|
|
||||||
|
|
||||||
item ??= new Genre();
|
|
||||||
|
|
||||||
if (userId is null || userId.Value.Equals(default))
|
|
||||||
{
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
var user = _userManager.GetUserById(userId.Value);
|
|
||||||
|
|
||||||
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
|
|
||||||
where T : BaseItem, new()
|
|
||||||
{
|
{
|
||||||
var result = libraryManager.GetItemList(new InternalItemsQuery
|
result = _libraryManager.GetGenres(query);
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '&'),
|
|
||||||
IncludeItemTypes = new[] { baseItemKind },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '/'),
|
|
||||||
IncludeItemTypes = new[] { baseItemKind },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
|
||||||
{
|
|
||||||
Name = name.Replace(BaseItem.SlugChar, '?'),
|
|
||||||
IncludeItemTypes = new[] { baseItemKind },
|
|
||||||
DtoOptions = dtoOptions
|
|
||||||
}).OfType<T>().FirstOrDefault();
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldIncludeItemTypes = includeItemTypes.Length != 0;
|
||||||
|
return RequestHelpers.CreateQueryResult(result, dtoOptions, _dtoService, shouldIncludeItemTypes, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a genre, by name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="genreName">The genre name.</param>
|
||||||
|
/// <param name="userId">The user id.</param>
|
||||||
|
/// <response code="200">Genres returned.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> containing the genre.</returns>
|
||||||
|
[HttpGet("{genreName}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<BaseItemDto> GetGenre([FromRoute, Required] string genreName, [FromQuery] Guid? userId)
|
||||||
|
{
|
||||||
|
var dtoOptions = new DtoOptions()
|
||||||
|
.AddClientFields(User);
|
||||||
|
|
||||||
|
Genre? item;
|
||||||
|
if (genreName.Contains(BaseItem.SlugChar, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
item = GetItemFromSlugName<Genre>(_libraryManager, genreName, dtoOptions, BaseItemKind.Genre);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
item = _libraryManager.GetGenre(genreName);
|
||||||
|
}
|
||||||
|
|
||||||
|
item ??= new Genre();
|
||||||
|
|
||||||
|
if (userId is null || userId.Value.Equals(default))
|
||||||
|
{
|
||||||
|
return _dtoService.GetBaseItemDto(item, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = _userManager.GetUserById(userId.Value);
|
||||||
|
|
||||||
|
return _dtoService.GetBaseItemDto(item, dtoOptions, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? GetItemFromSlugName<T>(ILibraryManager libraryManager, string name, DtoOptions dtoOptions, BaseItemKind baseItemKind)
|
||||||
|
where T : BaseItem, new()
|
||||||
|
{
|
||||||
|
var result = libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
Name = name.Replace(BaseItem.SlugChar, '&'),
|
||||||
|
IncludeItemTypes = new[] { baseItemKind },
|
||||||
|
DtoOptions = dtoOptions
|
||||||
|
}).OfType<T>().FirstOrDefault();
|
||||||
|
|
||||||
|
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
Name = name.Replace(BaseItem.SlugChar, '/'),
|
||||||
|
IncludeItemTypes = new[] { baseItemKind },
|
||||||
|
DtoOptions = dtoOptions
|
||||||
|
}).OfType<T>().FirstOrDefault();
|
||||||
|
|
||||||
|
result ??= libraryManager.GetItemList(new InternalItemsQuery
|
||||||
|
{
|
||||||
|
Name = name.Replace(BaseItem.SlugChar, '?'),
|
||||||
|
IncludeItemTypes = new[] { baseItemKind },
|
||||||
|
DtoOptions = dtoOptions
|
||||||
|
}).OfType<T>().FirstOrDefault();
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Attributes;
|
using Jellyfin.Api.Attributes;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Helpers;
|
using Jellyfin.Api.Helpers;
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
@ -15,178 +14,177 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hls segment controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
public class HlsSegmentController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The hls segment controller.
|
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class HlsSegmentController : BaseJellyfinApiController
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
/// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
|
||||||
|
public HlsSegmentController(
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IServerConfigurationManager serverConfigurationManager,
|
||||||
|
TranscodingJobHelper transcodingJobHelper)
|
||||||
{
|
{
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
_transcodingJobHelper = transcodingJobHelper;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
|
/// Gets the specified audio segment for an audio item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="segmentId">The segment id.</param>
|
||||||
/// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param>
|
/// <response code="200">Hls audio segment returned.</response>
|
||||||
public HlsSegmentController(
|
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
||||||
IFileSystem fileSystem,
|
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||||
IServerConfigurationManager serverConfigurationManager,
|
// [Authenticated]
|
||||||
TranscodingJobHelper transcodingJobHelper)
|
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
||||||
|
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesAudioFile]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
|
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||||
|
{
|
||||||
|
// TODO: Deprecate with new iOS app
|
||||||
|
var file = segmentId + Path.GetExtension(Request.Path);
|
||||||
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
|
||||||
{
|
{
|
||||||
_fileSystem = fileSystem;
|
return BadRequest("Invalid segment.");
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_transcodingJobHelper = transcodingJobHelper;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
|
||||||
/// Gets the specified audio segment for an audio item.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
/// <summary>
|
||||||
/// <param name="segmentId">The segment id.</param>
|
/// Gets a hls video playlist.
|
||||||
/// <response code="200">Hls audio segment returned.</response>
|
/// </summary>
|
||||||
/// <returns>A <see cref="FileStreamResult"/> containing the audio stream.</returns>
|
/// <param name="itemId">The video id.</param>
|
||||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
/// <param name="playlistId">The playlist id.</param>
|
||||||
// [Authenticated]
|
/// <response code="200">Hls video playlist returned.</response>
|
||||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.mp3", Name = "GetHlsAudioSegmentLegacyMp3")]
|
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
|
||||||
[HttpGet("Audio/{itemId}/hls/{segmentId}/stream.aac", Name = "GetHlsAudioSegmentLegacyAac")]
|
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[Authorize]
|
||||||
[ProducesAudioFile]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
[ProducesPlaylistFile]
|
||||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
|
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||||
|
{
|
||||||
|
var file = playlistId + Path.GetExtension(Request.Path);
|
||||||
|
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
||||||
{
|
{
|
||||||
// TODO: Deprecate with new iOS app
|
return BadRequest("Invalid segment.");
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
}
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
return GetFileResult(file, file);
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
}
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture))
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops an active encoding.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
||||||
|
/// <param name="playSessionId">The play session id.</param>
|
||||||
|
/// <response code="204">Encoding stopped successfully.</response>
|
||||||
|
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
||||||
|
[HttpDelete("Videos/ActiveEncodings")]
|
||||||
|
[Authorize]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public ActionResult StopEncodingProcess(
|
||||||
|
[FromQuery, Required] string deviceId,
|
||||||
|
[FromQuery, Required] string playSessionId)
|
||||||
|
{
|
||||||
|
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a hls video segment.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <param name="playlistId">The playlist id.</param>
|
||||||
|
/// <param name="segmentId">The segment id.</param>
|
||||||
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
|
/// <response code="200">Hls video segment returned.</response>
|
||||||
|
/// <response code="404">Hls segment not found.</response>
|
||||||
|
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
|
||||||
|
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
||||||
|
// [Authenticated]
|
||||||
|
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesVideoFile]
|
||||||
|
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||||
|
public ActionResult GetHlsVideoSegmentLegacy(
|
||||||
|
[FromRoute, Required] string itemId,
|
||||||
|
[FromRoute, Required] string playlistId,
|
||||||
|
[FromRoute, Required] string segmentId,
|
||||||
|
[FromRoute, Required] string segmentContainer)
|
||||||
|
{
|
||||||
|
var file = segmentId + Path.GetExtension(Request.Path);
|
||||||
|
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||||
|
|
||||||
|
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||||
|
var fileDir = Path.GetDirectoryName(file);
|
||||||
|
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid segment.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedPlaylistId = playlistId;
|
||||||
|
|
||||||
|
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
||||||
|
// Add . to start of segment container for future use.
|
||||||
|
segmentContainer = segmentContainer.Insert(0, ".");
|
||||||
|
string? playlistPath = null;
|
||||||
|
foreach (var path in filePaths)
|
||||||
|
{
|
||||||
|
var pathExtension = Path.GetExtension(path);
|
||||||
|
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||||
|
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
|
||||||
{
|
{
|
||||||
return BadRequest("Invalid segment.");
|
playlistPath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return playlistPath is null
|
||||||
|
? NotFound("Hls segment not found.")
|
||||||
|
: GetFileResult(file, playlistPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult GetFileResult(string path, string playlistPath)
|
||||||
|
{
|
||||||
|
var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
||||||
|
|
||||||
|
Response.OnCompleted(() =>
|
||||||
|
{
|
||||||
|
if (transcodingJob is not null)
|
||||||
|
{
|
||||||
|
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
|
||||||
}
|
}
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(file, MimeTypes.GetMimeType(file));
|
return Task.CompletedTask;
|
||||||
}
|
});
|
||||||
|
|
||||||
/// <summary>
|
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
|
||||||
/// Gets a hls video playlist.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The video id.</param>
|
|
||||||
/// <param name="playlistId">The playlist id.</param>
|
|
||||||
/// <response code="200">Hls video playlist returned.</response>
|
|
||||||
/// <returns>A <see cref="FileStreamResult"/> containing the playlist.</returns>
|
|
||||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/stream.m3u8")]
|
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesPlaylistFile]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
|
||||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
|
||||||
{
|
|
||||||
var file = playlistId + Path.GetExtension(Request.Path);
|
|
||||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid segment.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return GetFileResult(file, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops an active encoding.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="deviceId">The device id of the client requesting. Used to stop encoding processes when needed.</param>
|
|
||||||
/// <param name="playSessionId">The play session id.</param>
|
|
||||||
/// <response code="204">Encoding stopped successfully.</response>
|
|
||||||
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
|
|
||||||
[HttpDelete("Videos/ActiveEncodings")]
|
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public ActionResult StopEncodingProcess(
|
|
||||||
[FromQuery, Required] string deviceId,
|
|
||||||
[FromQuery, Required] string playSessionId)
|
|
||||||
{
|
|
||||||
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a hls video segment.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <param name="playlistId">The playlist id.</param>
|
|
||||||
/// <param name="segmentId">The segment id.</param>
|
|
||||||
/// <param name="segmentContainer">The segment container.</param>
|
|
||||||
/// <response code="200">Hls video segment returned.</response>
|
|
||||||
/// <response code="404">Hls segment not found.</response>
|
|
||||||
/// <returns>A <see cref="FileStreamResult"/> containing the video segment.</returns>
|
|
||||||
// Can't require authentication just yet due to seeing some requests come from Chrome without full query string
|
|
||||||
// [Authenticated]
|
|
||||||
[HttpGet("Videos/{itemId}/hls/{playlistId}/{segmentId}.{segmentContainer}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
[ProducesVideoFile]
|
|
||||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
|
||||||
public ActionResult GetHlsVideoSegmentLegacy(
|
|
||||||
[FromRoute, Required] string itemId,
|
|
||||||
[FromRoute, Required] string playlistId,
|
|
||||||
[FromRoute, Required] string segmentId,
|
|
||||||
[FromRoute, Required] string segmentContainer)
|
|
||||||
{
|
|
||||||
var file = segmentId + Path.GetExtension(Request.Path);
|
|
||||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
|
||||||
|
|
||||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
|
||||||
var fileDir = Path.GetDirectoryName(file);
|
|
||||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture))
|
|
||||||
{
|
|
||||||
return BadRequest("Invalid segment.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalizedPlaylistId = playlistId;
|
|
||||||
|
|
||||||
var filePaths = _fileSystem.GetFilePaths(transcodeFolderPath);
|
|
||||||
// Add . to start of segment container for future use.
|
|
||||||
segmentContainer = segmentContainer.Insert(0, ".");
|
|
||||||
string? playlistPath = null;
|
|
||||||
foreach (var path in filePaths)
|
|
||||||
{
|
|
||||||
var pathExtension = Path.GetExtension(path);
|
|
||||||
if ((string.Equals(pathExtension, segmentContainer, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(pathExtension, ".m3u8", StringComparison.OrdinalIgnoreCase))
|
|
||||||
&& path.IndexOf(normalizedPlaylistId, StringComparison.OrdinalIgnoreCase) != -1)
|
|
||||||
{
|
|
||||||
playlistPath = path;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playlistPath is null
|
|
||||||
? NotFound("Hls segment not found.")
|
|
||||||
: GetFileResult(file, playlistPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ActionResult GetFileResult(string path, string playlistPath)
|
|
||||||
{
|
|
||||||
var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
|
|
||||||
|
|
||||||
Response.OnCompleted(() =>
|
|
||||||
{
|
|
||||||
if (transcodingJob is not null)
|
|
||||||
{
|
|
||||||
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
|
|
||||||
return FileStreamResponseHelpers.GetStaticFileResult(path, MimeTypes.GetMimeType(path));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Jellyfin.Api.Constants;
|
|
||||||
using Jellyfin.Api.Extensions;
|
using Jellyfin.Api.Extensions;
|
||||||
using Jellyfin.Api.ModelBinders;
|
using Jellyfin.Api.ModelBinders;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
@ -16,346 +15,345 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The instant mix controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
[Authorize]
|
||||||
|
public class InstantMixController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IUserManager _userManager;
|
||||||
|
private readonly IDtoService _dtoService;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IMusicManager _musicManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The instant mix controller.
|
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||||
public class InstantMixController : BaseJellyfinApiController
|
/// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
|
||||||
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
public InstantMixController(
|
||||||
|
IUserManager userManager,
|
||||||
|
IDtoService dtoService,
|
||||||
|
IMusicManager musicManager,
|
||||||
|
ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
private readonly IUserManager _userManager;
|
_userManager = userManager;
|
||||||
private readonly IDtoService _dtoService;
|
_dtoService = dtoService;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_musicManager = musicManager;
|
||||||
private readonly IMusicManager _musicManager;
|
_libraryManager = libraryManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="InstantMixController"/> class.
|
/// Creates an instant playlist based on a given song.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
/// <param name="id">The item id.</param>
|
||||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||||
/// <param name="musicManager">Instance of the <see cref="IMusicManager"/> interface.</param>
|
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||||
public InstantMixController(
|
/// <param name="enableImages">Optional. Include image information in output.</param>
|
||||||
IUserManager userManager,
|
/// <param name="enableUserData">Optional. Include user data.</param>
|
||||||
IDtoService dtoService,
|
/// <param name="imageTypeLimit">Optional. The max number of images to return, per image type.</param>
|
||||||
IMusicManager musicManager,
|
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||||
ILibraryManager libraryManager)
|
/// <response code="200">Instant playlist returned.</response>
|
||||||
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||||
|
[HttpGet("Songs/{id}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
|
||||||
|
[FromRoute, 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)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given album.
|
||||||
|
/// </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("Albums/{id}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
|
||||||
|
[FromRoute, 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)
|
||||||
|
{
|
||||||
|
var album = _libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
|
||||||
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given playlist.
|
||||||
|
/// </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("Playlists/{id}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
|
||||||
|
[FromRoute, 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)
|
||||||
|
{
|
||||||
|
var playlist = (Playlist)_libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
|
||||||
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given genre.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The genre name.</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/{name}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
|
||||||
|
[FromRoute, Required] string name,
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, 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/{id}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||||
|
[FromRoute, 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)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an instant playlist based on a given item.
|
||||||
|
/// </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("Items/{id}/InstantMix")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
|
||||||
|
[FromRoute, 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)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(item, user, 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)]
|
||||||
|
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
|
||||||
|
[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)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(id);
|
||||||
|
var user = userId is null || userId.Value.Equals(default)
|
||||||
|
? null
|
||||||
|
: _userManager.GetUserById(userId.Value);
|
||||||
|
var dtoOptions = new DtoOptions { Fields = fields }
|
||||||
|
.AddClientFields(User)
|
||||||
|
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
||||||
|
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
||||||
|
return GetResult(items, user, limit, dtoOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
||||||
|
{
|
||||||
|
var list = items;
|
||||||
|
|
||||||
|
var totalCount = list.Count;
|
||||||
|
|
||||||
|
if (limit.HasValue && limit < list.Count)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
list = list.GetRange(0, limit.Value);
|
||||||
_dtoService = dtoService;
|
|
||||||
_musicManager = musicManager;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
||||||
/// Creates an instant playlist based on a given song.
|
|
||||||
/// </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("Songs/{id}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
|
|
||||||
[FromRoute, 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)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
var result = new QueryResult<BaseItemDto>(
|
||||||
/// Creates an instant playlist based on a given album.
|
0,
|
||||||
/// </summary>
|
totalCount,
|
||||||
/// <param name="id">The item id.</param>
|
returnList);
|
||||||
/// <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("Albums/{id}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
|
|
||||||
[FromRoute, 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)
|
|
||||||
{
|
|
||||||
var album = _libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(album, user, dtoOptions);
|
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
return result;
|
||||||
/// Creates an instant playlist based on a given playlist.
|
|
||||||
/// </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("Playlists/{id}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
|
|
||||||
[FromRoute, 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)
|
|
||||||
{
|
|
||||||
var playlist = (Playlist)_libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(playlist, user, dtoOptions);
|
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an instant playlist based on a given genre.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">The genre name.</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/{name}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreByName(
|
|
||||||
[FromRoute, Required] string name,
|
|
||||||
[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)
|
|
||||||
{
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromGenres(new[] { name }, user, 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/{id}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
|
||||||
[FromRoute, 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)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates an instant playlist based on a given item.
|
|
||||||
/// </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("Items/{id}/InstantMix")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
|
|
||||||
[FromRoute, 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)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, 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)]
|
|
||||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromMusicGenreById(
|
|
||||||
[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)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(id);
|
|
||||||
var user = userId is null || userId.Value.Equals(default)
|
|
||||||
? null
|
|
||||||
: _userManager.GetUserById(userId.Value);
|
|
||||||
var dtoOptions = new DtoOptions { Fields = fields }
|
|
||||||
.AddClientFields(User)
|
|
||||||
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
|
|
||||||
var items = _musicManager.GetInstantMixFromItem(item, user, dtoOptions);
|
|
||||||
return GetResult(items, user, limit, dtoOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private QueryResult<BaseItemDto> GetResult(List<BaseItem> items, User? user, int? limit, DtoOptions dtoOptions)
|
|
||||||
{
|
|
||||||
var list = items;
|
|
||||||
|
|
||||||
var totalCount = list.Count;
|
|
||||||
|
|
||||||
if (limit.HasValue && limit < list.Count)
|
|
||||||
{
|
|
||||||
list = list.GetRange(0, limit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
var returnList = _dtoService.GetBaseItemDtos(list, dtoOptions, user);
|
|
||||||
|
|
||||||
var result = new QueryResult<BaseItemDto>(
|
|
||||||
0,
|
|
||||||
totalCount,
|
|
||||||
returnList);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Api.Constants;
|
using Jellyfin.Api.Constants;
|
||||||
@ -18,257 +17,256 @@ using Microsoft.AspNetCore.Http;
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item lookup controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
[Authorize]
|
||||||
|
public class ItemLookupController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly ILogger<ItemLookupController> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Item lookup controller.
|
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.DefaultAuthorization)]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
public class ItemLookupController : BaseJellyfinApiController
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
|
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
|
||||||
|
public ItemLookupController(
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
ILogger<ItemLookupController> logger)
|
||||||
{
|
{
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly ILogger<ItemLookupController> _logger;
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemLookupController"/> class.
|
/// Get the item's external id info.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <response code="200">External id info retrieved.</response>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <param name="logger">Instance of the <see cref="ILogger{ItemLookupController}"/> interface.</param>
|
/// <returns>List of external id info.</returns>
|
||||||
public ItemLookupController(
|
[HttpGet("Items/{itemId}/ExternalIdInfos")]
|
||||||
IProviderManager providerManager,
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
IFileSystem fileSystem,
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
ILibraryManager libraryManager,
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
ILogger<ItemLookupController> logger)
|
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
{
|
{
|
||||||
_providerManager = providerManager;
|
return NotFound();
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_libraryManager = libraryManager;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
return Ok(_providerManager.GetExternalIdInfos(item));
|
||||||
/// Get the item's external id info.
|
}
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">Item id.</param>
|
/// <summary>
|
||||||
/// <response code="200">External id info retrieved.</response>
|
/// Get movie remote search.
|
||||||
/// <response code="404">Item not found.</response>
|
/// </summary>
|
||||||
/// <returns>List of external id info.</returns>
|
/// <param name="query">Remote search query.</param>
|
||||||
[HttpGet("Items/{itemId}/ExternalIdInfos")]
|
/// <response code="200">Movie remote search executed.</response>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <returns>
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
public ActionResult<IEnumerable<ExternalIdInfo>> GetExternalIdInfos([FromRoute, Required] Guid itemId)
|
/// </returns>
|
||||||
{
|
[HttpPost("Items/RemoteSearch/Movie")]
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
|
||||||
if (item is null)
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get trailer remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Trailer remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/Trailer")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get music video remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Music video remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/MusicVideo")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get series remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Series remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/Series")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get box set remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Box set remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/BoxSet")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get music artist remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Music artist remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/MusicArtist")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get music album remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Music album remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/MusicAlbum")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get person remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Person remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/Person")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get book remote search.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">Remote search query.</param>
|
||||||
|
/// <response code="200">Book remote search executed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/Book")]
|
||||||
|
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
|
||||||
|
{
|
||||||
|
var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
return Ok(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies search criteria to an item and refreshes metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">Item id.</param>
|
||||||
|
/// <param name="searchResult">The remote search result.</param>
|
||||||
|
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
|
||||||
|
/// <response code="204">Item metadata refreshed.</response>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
||||||
|
/// The task result contains an <see cref="NoContentResult"/>.
|
||||||
|
/// </returns>
|
||||||
|
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
public async Task<ActionResult> ApplySearchCriteria(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromBody, Required] RemoteSearchResult searchResult,
|
||||||
|
[FromQuery] bool replaceAllImages = true)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
|
||||||
|
item.Id,
|
||||||
|
item.Name,
|
||||||
|
searchResult.ProviderIds);
|
||||||
|
|
||||||
|
// Since the refresh process won't erase provider Ids, we need to set this explicitly now.
|
||||||
|
item.ProviderIds = searchResult.ProviderIds;
|
||||||
|
await _providerManager.RefreshFullItem(
|
||||||
|
item,
|
||||||
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
{
|
{
|
||||||
return NotFound();
|
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
}
|
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
|
ReplaceAllMetadata = true,
|
||||||
|
ReplaceAllImages = replaceAllImages,
|
||||||
|
SearchResult = searchResult,
|
||||||
|
RemoveOldMetadata = true
|
||||||
|
},
|
||||||
|
CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
return Ok(_providerManager.GetExternalIdInfos(item));
|
return NoContent();
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get movie remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Movie remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Movie")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMovieRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MovieInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<Movie, MovieInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get trailer remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Trailer remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Trailer")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetTrailerRemoteSearchResults([FromBody, Required] RemoteSearchQuery<TrailerInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<Trailer, TrailerInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get music video remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Music video remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/MusicVideo")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicVideoRemoteSearchResults([FromBody, Required] RemoteSearchQuery<MusicVideoInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<MusicVideo, MusicVideoInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get series remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Series remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Series")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetSeriesRemoteSearchResults([FromBody, Required] RemoteSearchQuery<SeriesInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<Series, SeriesInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get box set remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Box set remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/BoxSet")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBoxSetRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BoxSetInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<BoxSet, BoxSetInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get music artist remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Music artist remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/MusicArtist")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicArtistRemoteSearchResults([FromBody, Required] RemoteSearchQuery<ArtistInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<MusicArtist, ArtistInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get music album remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Music album remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/MusicAlbum")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetMusicAlbumRemoteSearchResults([FromBody, Required] RemoteSearchQuery<AlbumInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<MusicAlbum, AlbumInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get person remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Person remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Person")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetPersonRemoteSearchResults([FromBody, Required] RemoteSearchQuery<PersonLookupInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<Person, PersonLookupInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get book remote search.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">Remote search query.</param>
|
|
||||||
/// <response code="200">Book remote search executed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="OkResult"/> containing the list of remote search results.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Book")]
|
|
||||||
public async Task<ActionResult<IEnumerable<RemoteSearchResult>>> GetBookRemoteSearchResults([FromBody, Required] RemoteSearchQuery<BookInfo> query)
|
|
||||||
{
|
|
||||||
var results = await _providerManager.GetRemoteSearchResults<Book, BookInfo>(query, CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return Ok(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies search criteria to an item and refreshes metadata.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">Item id.</param>
|
|
||||||
/// <param name="searchResult">The remote search result.</param>
|
|
||||||
/// <param name="replaceAllImages">Optional. Whether or not to replace all images. Default: True.</param>
|
|
||||||
/// <response code="204">Item metadata refreshed.</response>
|
|
||||||
/// <returns>
|
|
||||||
/// A <see cref="Task" /> that represents the asynchronous operation to get the remote search results.
|
|
||||||
/// The task result contains an <see cref="NoContentResult"/>.
|
|
||||||
/// </returns>
|
|
||||||
[HttpPost("Items/RemoteSearch/Apply/{itemId}")]
|
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
public async Task<ActionResult> ApplySearchCriteria(
|
|
||||||
[FromRoute, Required] Guid itemId,
|
|
||||||
[FromBody, Required] RemoteSearchResult searchResult,
|
|
||||||
[FromQuery] bool replaceAllImages = true)
|
|
||||||
{
|
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Setting provider id's to item {ItemId}-{ItemName}: {@ProviderIds}",
|
|
||||||
item.Id,
|
|
||||||
item.Name,
|
|
||||||
searchResult.ProviderIds);
|
|
||||||
|
|
||||||
// Since the refresh process won't erase provider Ids, we need to set this explicitly now.
|
|
||||||
item.ProviderIds = searchResult.ProviderIds;
|
|
||||||
await _providerManager.RefreshFullItem(
|
|
||||||
item,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ReplaceAllMetadata = true,
|
|
||||||
ReplaceAllImages = replaceAllImages,
|
|
||||||
SearchResult = searchResult,
|
|
||||||
RemoveOldMetadata = true
|
|
||||||
},
|
|
||||||
CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,78 +9,77 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item Refresh Controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("Items")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public class ItemRefreshController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Item Refresh Controller.
|
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("Items")]
|
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
||||||
public class ItemRefreshController : BaseJellyfinApiController
|
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||||
|
public ItemRefreshController(
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
|
/// Refreshes metadata for an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="itemId">Item id.</param>
|
||||||
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
|
/// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
|
||||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
||||||
public ItemRefreshController(
|
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||||
ILibraryManager libraryManager,
|
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
|
||||||
IProviderManager providerManager,
|
/// <response code="204">Item metadata refresh queued.</response>
|
||||||
IFileSystem fileSystem)
|
/// <response code="404">Item to refresh not found.</response>
|
||||||
|
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||||
|
[HttpPost("{itemId}/Refresh")]
|
||||||
|
[Description("Refreshes metadata for an item.")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult RefreshItem(
|
||||||
|
[FromRoute, Required] Guid itemId,
|
||||||
|
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
|
||||||
|
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
||||||
|
[FromQuery] bool replaceAllMetadata = false,
|
||||||
|
[FromQuery] bool replaceAllImages = false)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
return NotFound();
|
||||||
_providerManager = providerManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
/// Refreshes metadata for an item.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">Item id.</param>
|
|
||||||
/// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
|
|
||||||
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
|
|
||||||
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
|
|
||||||
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
|
|
||||||
/// <response code="204">Item metadata refresh queued.</response>
|
|
||||||
/// <response code="404">Item to refresh not found.</response>
|
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
|
||||||
[HttpPost("{itemId}/Refresh")]
|
|
||||||
[Description("Refreshes metadata for an item.")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult RefreshItem(
|
|
||||||
[FromRoute, Required] Guid itemId,
|
|
||||||
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
|
|
||||||
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
|
|
||||||
[FromQuery] bool replaceAllMetadata = false,
|
|
||||||
[FromQuery] bool replaceAllImages = false)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
MetadataRefreshMode = metadataRefreshMode,
|
||||||
if (item is null)
|
ImageRefreshMode = imageRefreshMode,
|
||||||
{
|
ReplaceAllImages = replaceAllImages,
|
||||||
return NotFound();
|
ReplaceAllMetadata = replaceAllMetadata,
|
||||||
}
|
ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
||||||
|
|| replaceAllImages
|
||||||
|
|| replaceAllMetadata,
|
||||||
|
IsAutomated = false
|
||||||
|
};
|
||||||
|
|
||||||
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
||||||
{
|
return NoContent();
|
||||||
MetadataRefreshMode = metadataRefreshMode,
|
|
||||||
ImageRefreshMode = imageRefreshMode,
|
|
||||||
ReplaceAllImages = replaceAllImages,
|
|
||||||
ReplaceAllMetadata = replaceAllMetadata,
|
|
||||||
ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
|
|
||||||
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|
|
||||||
|| replaceAllImages
|
|
||||||
|| replaceAllMetadata,
|
|
||||||
IsAutomated = false
|
|
||||||
};
|
|
||||||
|
|
||||||
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,332 +20,332 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Jellyfin.Api.Controllers
|
namespace Jellyfin.Api.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item update controller.
|
||||||
|
/// </summary>
|
||||||
|
[Route("")]
|
||||||
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
|
public class ItemUpdateController : BaseJellyfinApiController
|
||||||
{
|
{
|
||||||
|
private readonly ILibraryManager _libraryManager;
|
||||||
|
private readonly IProviderManager _providerManager;
|
||||||
|
private readonly ILocalizationManager _localizationManager;
|
||||||
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Item update controller.
|
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Route("")]
|
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||||
public class ItemUpdateController : BaseJellyfinApiController
|
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||||
|
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||||
|
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
public ItemUpdateController(
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
ILibraryManager libraryManager,
|
||||||
|
IProviderManager providerManager,
|
||||||
|
ILocalizationManager localizationManager,
|
||||||
|
IServerConfigurationManager serverConfigurationManager)
|
||||||
{
|
{
|
||||||
private readonly ILibraryManager _libraryManager;
|
_libraryManager = libraryManager;
|
||||||
private readonly IProviderManager _providerManager;
|
_providerManager = providerManager;
|
||||||
private readonly ILocalizationManager _localizationManager;
|
_localizationManager = localizationManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
_fileSystem = fileSystem;
|
||||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="ItemUpdateController"/> class.
|
/// Updates an item.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
/// <param name="itemId">The item id.</param>
|
||||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
/// <param name="request">The new item properties.</param>
|
||||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
/// <response code="204">Item updated.</response>
|
||||||
/// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
/// <response code="404">Item not found.</response>
|
||||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||||
public ItemUpdateController(
|
[HttpPost("Items/{itemId}")]
|
||||||
IFileSystem fileSystem,
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
ILibraryManager libraryManager,
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
IProviderManager providerManager,
|
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
|
||||||
ILocalizationManager localizationManager,
|
{
|
||||||
IServerConfigurationManager serverConfigurationManager)
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
{
|
{
|
||||||
_libraryManager = libraryManager;
|
return NotFound();
|
||||||
_providerManager = providerManager;
|
|
||||||
_localizationManager = localizationManager;
|
|
||||||
_fileSystem = fileSystem;
|
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var newLockData = request.LockData ?? false;
|
||||||
/// Updates an item.
|
var isLockedChanged = item.IsLocked != newLockData;
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
var series = item as Series;
|
||||||
/// <param name="request">The new item properties.</param>
|
var displayOrderChanged = series is not null && !string.Equals(
|
||||||
/// <response code="204">Item updated.</response>
|
series.DisplayOrder ?? string.Empty,
|
||||||
/// <response code="404">Item not found.</response>
|
request.DisplayOrder ?? string.Empty,
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
StringComparison.OrdinalIgnoreCase);
|
||||||
[HttpPost("Items/{itemId}")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
// Do this first so that metadata savers can pull the updates from the database.
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
if (request.People is not null)
|
||||||
public async Task<ActionResult> UpdateItem([FromRoute, Required] Guid itemId, [FromBody, Required] BaseItemDto request)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
_libraryManager.UpdatePeople(
|
||||||
if (item is null)
|
item,
|
||||||
{
|
request.People.Select(x => new PersonInfo
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var newLockData = request.LockData ?? false;
|
|
||||||
var isLockedChanged = item.IsLocked != newLockData;
|
|
||||||
|
|
||||||
var series = item as Series;
|
|
||||||
var displayOrderChanged = series is not null && !string.Equals(
|
|
||||||
series.DisplayOrder ?? string.Empty,
|
|
||||||
request.DisplayOrder ?? string.Empty,
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
// Do this first so that metadata savers can pull the updates from the database.
|
|
||||||
if (request.People is not null)
|
|
||||||
{
|
|
||||||
_libraryManager.UpdatePeople(
|
|
||||||
item,
|
|
||||||
request.People.Select(x => new PersonInfo
|
|
||||||
{
|
|
||||||
Name = x.Name,
|
|
||||||
Role = x.Role,
|
|
||||||
Type = x.Type
|
|
||||||
}).ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateItem(request, item);
|
|
||||||
|
|
||||||
item.OnMetadataChanged();
|
|
||||||
|
|
||||||
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (isLockedChanged && item.IsFolder)
|
|
||||||
{
|
|
||||||
var folder = (Folder)item;
|
|
||||||
|
|
||||||
foreach (var child in folder.GetRecursiveChildren())
|
|
||||||
{
|
{
|
||||||
child.IsLocked = newLockData;
|
Name = x.Name,
|
||||||
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
Role = x.Role,
|
||||||
}
|
Type = x.Type
|
||||||
}
|
}).ToList());
|
||||||
|
|
||||||
if (displayOrderChanged)
|
|
||||||
{
|
|
||||||
_providerManager.QueueRefresh(
|
|
||||||
series!.Id,
|
|
||||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
|
||||||
{
|
|
||||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
|
||||||
ReplaceAllMetadata = true
|
|
||||||
},
|
|
||||||
RefreshPriority.High);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
UpdateItem(request, item);
|
||||||
/// Gets metadata editor info for an item.
|
|
||||||
/// </summary>
|
item.OnMetadataChanged();
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <response code="200">Item metadata editor returned.</response>
|
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
/// <response code="404">Item not found.</response>
|
|
||||||
/// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
if (isLockedChanged && item.IsFolder)
|
||||||
[HttpGet("Items/{itemId}/MetadataEditor")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
var folder = (Folder)item;
|
||||||
|
|
||||||
var info = new MetadataEditorInfo
|
foreach (var child in folder.GetRecursiveChildren())
|
||||||
{
|
{
|
||||||
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
child.IsLocked = newLockData;
|
||||||
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
await child.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);
|
||||||
Countries = _localizationManager.GetCountries().ToArray(),
|
|
||||||
Cultures = _localizationManager.GetCultures().ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!item.IsVirtualItem
|
|
||||||
&& item is not ICollectionFolder
|
|
||||||
&& item is not UserView
|
|
||||||
&& item is not AggregateFolder
|
|
||||||
&& item is not LiveTvChannel
|
|
||||||
&& item is not IItemByName
|
|
||||||
&& item.SourceType == SourceType.Library)
|
|
||||||
{
|
|
||||||
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
|
|
||||||
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(inheritedContentType) ||
|
|
||||||
!string.IsNullOrWhiteSpace(configuredContentType))
|
|
||||||
{
|
|
||||||
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
|
||||||
info.ContentType = configuredContentType;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(inheritedContentType)
|
|
||||||
|| string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
info.ContentTypeOptions = info.ContentTypeOptions
|
|
||||||
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
|
||||||
|| string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
if (displayOrderChanged)
|
||||||
/// Updates an item's content type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="itemId">The item id.</param>
|
|
||||||
/// <param name="contentType">The content type of the item.</param>
|
|
||||||
/// <response code="204">Item content type updated.</response>
|
|
||||||
/// <response code="404">Item not found.</response>
|
|
||||||
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
|
||||||
[HttpPost("Items/{itemId}/ContentType")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
||||||
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
|
|
||||||
{
|
{
|
||||||
var item = _libraryManager.GetItemById(itemId);
|
_providerManager.QueueRefresh(
|
||||||
if (item is null)
|
series!.Id,
|
||||||
{
|
new MetadataRefreshOptions(new DirectoryService(_fileSystem))
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var path = item.ContainingFolderPath;
|
|
||||||
|
|
||||||
var types = _serverConfigurationManager.Configuration.ContentTypes
|
|
||||||
.Where(i => !string.IsNullOrWhiteSpace(i.Name))
|
|
||||||
.Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(contentType))
|
|
||||||
{
|
|
||||||
types.Add(new NameValuePair
|
|
||||||
{
|
{
|
||||||
Name = path,
|
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
Value = contentType
|
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
|
||||||
});
|
ReplaceAllMetadata = true
|
||||||
}
|
},
|
||||||
|
RefreshPriority.High);
|
||||||
_serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
|
|
||||||
_serverConfigurationManager.SaveConfiguration();
|
|
||||||
return NoContent();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateItem(BaseItemDto request, BaseItem item)
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets metadata editor info for an item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <response code="200">Item metadata editor returned.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>An <see cref="OkResult"/> on success containing the metadata editor, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||||
|
[HttpGet("Items/{itemId}/MetadataEditor")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult<MetadataEditorInfo> GetMetadataEditorInfo([FromRoute, Required] Guid itemId)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
|
||||||
|
var info = new MetadataEditorInfo
|
||||||
{
|
{
|
||||||
item.Name = request.Name;
|
ParentalRatingOptions = _localizationManager.GetParentalRatings().ToArray(),
|
||||||
item.ForcedSortName = request.ForcedSortName;
|
ExternalIdInfos = _providerManager.GetExternalIdInfos(item).ToArray(),
|
||||||
|
Countries = _localizationManager.GetCountries().ToArray(),
|
||||||
|
Cultures = _localizationManager.GetCultures().ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
|
if (!item.IsVirtualItem
|
||||||
|
&& item is not ICollectionFolder
|
||||||
|
&& item is not UserView
|
||||||
|
&& item is not AggregateFolder
|
||||||
|
&& item is not LiveTvChannel
|
||||||
|
&& item is not IItemByName
|
||||||
|
&& item.SourceType == SourceType.Library)
|
||||||
|
{
|
||||||
|
var inheritedContentType = _libraryManager.GetInheritedContentType(item);
|
||||||
|
var configuredContentType = _libraryManager.GetConfiguredContentType(item);
|
||||||
|
|
||||||
item.CriticRating = request.CriticRating;
|
if (string.IsNullOrWhiteSpace(inheritedContentType) ||
|
||||||
|
!string.IsNullOrWhiteSpace(configuredContentType))
|
||||||
item.CommunityRating = request.CommunityRating;
|
|
||||||
item.IndexNumber = request.IndexNumber;
|
|
||||||
item.ParentIndexNumber = request.ParentIndexNumber;
|
|
||||||
item.Overview = request.Overview;
|
|
||||||
item.Genres = request.Genres;
|
|
||||||
|
|
||||||
if (item is Episode episode)
|
|
||||||
{
|
{
|
||||||
episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
|
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
|
||||||
episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
|
info.ContentType = configuredContentType;
|
||||||
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.Tags = request.Tags;
|
if (string.IsNullOrWhiteSpace(inheritedContentType)
|
||||||
|
|| string.Equals(inheritedContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
if (request.Taglines is not null)
|
|
||||||
{
|
|
||||||
item.Tagline = request.Taglines.FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.Studios is not null)
|
|
||||||
{
|
|
||||||
item.Studios = request.Studios.Select(x => x.Name).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.DateCreated.HasValue)
|
|
||||||
{
|
|
||||||
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
|
|
||||||
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
|
|
||||||
item.ProductionYear = request.ProductionYear;
|
|
||||||
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
|
|
||||||
item.CustomRating = request.CustomRating;
|
|
||||||
|
|
||||||
if (request.ProductionLocations is not null)
|
|
||||||
{
|
|
||||||
item.ProductionLocations = request.ProductionLocations;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
|
|
||||||
item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
|
|
||||||
|
|
||||||
if (item is IHasDisplayOrder hasDisplayOrder)
|
|
||||||
{
|
|
||||||
hasDisplayOrder.DisplayOrder = request.DisplayOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is IHasAspectRatio hasAspectRatio)
|
|
||||||
{
|
|
||||||
hasAspectRatio.AspectRatio = request.AspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
item.IsLocked = request.LockData ?? false;
|
|
||||||
|
|
||||||
if (request.LockedFields is not null)
|
|
||||||
{
|
|
||||||
item.LockedFields = request.LockedFields;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only allow this for series. Runtimes for media comes from ffprobe.
|
|
||||||
if (item is Series)
|
|
||||||
{
|
|
||||||
item.RunTimeTicks = request.RunTimeTicks;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var pair in request.ProviderIds.ToList())
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(pair.Value))
|
|
||||||
{
|
{
|
||||||
request.ProviderIds.Remove(pair.Key);
|
info.ContentTypeOptions = info.ContentTypeOptions
|
||||||
}
|
.Where(i => string.IsNullOrWhiteSpace(i.Value)
|
||||||
}
|
|| string.Equals(i.Value, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
|
||||||
|
|
||||||
item.ProviderIds = request.ProviderIds;
|
|
||||||
|
|
||||||
if (item is Video video)
|
|
||||||
{
|
|
||||||
video.Video3DFormat = request.Video3DFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.AlbumArtists is not null)
|
|
||||||
{
|
|
||||||
if (item is IHasAlbumArtist hasAlbumArtists)
|
|
||||||
{
|
|
||||||
hasAlbumArtists.AlbumArtists = request
|
|
||||||
.AlbumArtists
|
|
||||||
.Select(i => i.Name)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (request.ArtistItems is not null)
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates an item's content type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The item id.</param>
|
||||||
|
/// <param name="contentType">The content type of the item.</param>
|
||||||
|
/// <response code="204">Item content type updated.</response>
|
||||||
|
/// <response code="404">Item not found.</response>
|
||||||
|
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
|
||||||
|
[HttpPost("Items/{itemId}/ContentType")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
|
public ActionResult UpdateItemContentType([FromRoute, Required] Guid itemId, [FromQuery] string? contentType)
|
||||||
|
{
|
||||||
|
var item = _libraryManager.GetItemById(itemId);
|
||||||
|
if (item is null)
|
||||||
|
{
|
||||||
|
return NotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = item.ContainingFolderPath;
|
||||||
|
|
||||||
|
var types = _serverConfigurationManager.Configuration.ContentTypes
|
||||||
|
.Where(i => !string.IsNullOrWhiteSpace(i.Name))
|
||||||
|
.Where(i => !string.Equals(i.Name, path, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(contentType))
|
||||||
|
{
|
||||||
|
types.Add(new NameValuePair
|
||||||
{
|
{
|
||||||
if (item is IHasArtist hasArtists)
|
Name = path,
|
||||||
{
|
Value = contentType
|
||||||
hasArtists.Artists = request
|
});
|
||||||
.ArtistItems
|
}
|
||||||
.Select(i => i.Name)
|
|
||||||
.ToArray();
|
_serverConfigurationManager.Configuration.ContentTypes = types.ToArray();
|
||||||
}
|
_serverConfigurationManager.SaveConfiguration();
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateItem(BaseItemDto request, BaseItem item)
|
||||||
|
{
|
||||||
|
item.Name = request.Name;
|
||||||
|
item.ForcedSortName = request.ForcedSortName;
|
||||||
|
|
||||||
|
item.OriginalTitle = string.IsNullOrWhiteSpace(request.OriginalTitle) ? null : request.OriginalTitle;
|
||||||
|
|
||||||
|
item.CriticRating = request.CriticRating;
|
||||||
|
|
||||||
|
item.CommunityRating = request.CommunityRating;
|
||||||
|
item.IndexNumber = request.IndexNumber;
|
||||||
|
item.ParentIndexNumber = request.ParentIndexNumber;
|
||||||
|
item.Overview = request.Overview;
|
||||||
|
item.Genres = request.Genres;
|
||||||
|
|
||||||
|
if (item is Episode episode)
|
||||||
|
{
|
||||||
|
episode.AirsAfterSeasonNumber = request.AirsAfterSeasonNumber;
|
||||||
|
episode.AirsBeforeEpisodeNumber = request.AirsBeforeEpisodeNumber;
|
||||||
|
episode.AirsBeforeSeasonNumber = request.AirsBeforeSeasonNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Tags = request.Tags;
|
||||||
|
|
||||||
|
if (request.Taglines is not null)
|
||||||
|
{
|
||||||
|
item.Tagline = request.Taglines.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.Studios is not null)
|
||||||
|
{
|
||||||
|
item.Studios = request.Studios.Select(x => x.Name).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.DateCreated.HasValue)
|
||||||
|
{
|
||||||
|
item.DateCreated = NormalizeDateTime(request.DateCreated.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
item.EndDate = request.EndDate.HasValue ? NormalizeDateTime(request.EndDate.Value) : null;
|
||||||
|
item.PremiereDate = request.PremiereDate.HasValue ? NormalizeDateTime(request.PremiereDate.Value) : null;
|
||||||
|
item.ProductionYear = request.ProductionYear;
|
||||||
|
item.OfficialRating = string.IsNullOrWhiteSpace(request.OfficialRating) ? null : request.OfficialRating;
|
||||||
|
item.CustomRating = request.CustomRating;
|
||||||
|
|
||||||
|
if (request.ProductionLocations is not null)
|
||||||
|
{
|
||||||
|
item.ProductionLocations = request.ProductionLocations;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.PreferredMetadataCountryCode = request.PreferredMetadataCountryCode;
|
||||||
|
item.PreferredMetadataLanguage = request.PreferredMetadataLanguage;
|
||||||
|
|
||||||
|
if (item is IHasDisplayOrder hasDisplayOrder)
|
||||||
|
{
|
||||||
|
hasDisplayOrder.DisplayOrder = request.DisplayOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is IHasAspectRatio hasAspectRatio)
|
||||||
|
{
|
||||||
|
hasAspectRatio.AspectRatio = request.AspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.IsLocked = request.LockData ?? false;
|
||||||
|
|
||||||
|
if (request.LockedFields is not null)
|
||||||
|
{
|
||||||
|
item.LockedFields = request.LockedFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow this for series. Runtimes for media comes from ffprobe.
|
||||||
|
if (item is Series)
|
||||||
|
{
|
||||||
|
item.RunTimeTicks = request.RunTimeTicks;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in request.ProviderIds.ToList())
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(pair.Value))
|
||||||
|
{
|
||||||
|
request.ProviderIds.Remove(pair.Key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (item)
|
item.ProviderIds = request.ProviderIds;
|
||||||
|
|
||||||
|
if (item is Video video)
|
||||||
|
{
|
||||||
|
video.Video3DFormat = request.Video3DFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.AlbumArtists is not null)
|
||||||
|
{
|
||||||
|
if (item is IHasAlbumArtist hasAlbumArtists)
|
||||||
{
|
{
|
||||||
case Audio song:
|
hasAlbumArtists.AlbumArtists = request
|
||||||
song.Album = request.Album;
|
.AlbumArtists
|
||||||
break;
|
.Select(i => i.Name)
|
||||||
case MusicVideo musicVideo:
|
.ToArray();
|
||||||
musicVideo.Album = request.Album;
|
}
|
||||||
break;
|
}
|
||||||
case Series series:
|
|
||||||
|
if (request.ArtistItems is not null)
|
||||||
|
{
|
||||||
|
if (item is IHasArtist hasArtists)
|
||||||
|
{
|
||||||
|
hasArtists.Artists = request
|
||||||
|
.ArtistItems
|
||||||
|
.Select(i => i.Name)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (item)
|
||||||
|
{
|
||||||
|
case Audio song:
|
||||||
|
song.Album = request.Album;
|
||||||
|
break;
|
||||||
|
case MusicVideo musicVideo:
|
||||||
|
musicVideo.Album = request.Album;
|
||||||
|
break;
|
||||||
|
case Series series:
|
||||||
{
|
{
|
||||||
series.Status = GetSeriesStatus(request);
|
series.Status = GetSeriesStatus(request);
|
||||||
|
|
||||||
@ -357,93 +357,92 @@ namespace Jellyfin.Api.Controllers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SeriesStatus? GetSeriesStatus(BaseItemDto item)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(item.Status))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DateTime NormalizeDateTime(DateTime val)
|
|
||||||
{
|
|
||||||
return DateTime.SpecifyKind(val, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<NameValuePair> GetContentTypeOptions(bool isForItem)
|
|
||||||
{
|
|
||||||
var list = new List<NameValuePair>();
|
|
||||||
|
|
||||||
if (isForItem)
|
|
||||||
{
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Inherit",
|
|
||||||
Value = string.Empty
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Movies",
|
|
||||||
Value = "movies"
|
|
||||||
});
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Music",
|
|
||||||
Value = "music"
|
|
||||||
});
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Shows",
|
|
||||||
Value = "tvshows"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isForItem)
|
|
||||||
{
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Books",
|
|
||||||
Value = "books"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "HomeVideos",
|
|
||||||
Value = "homevideos"
|
|
||||||
});
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "MusicVideos",
|
|
||||||
Value = "musicvideos"
|
|
||||||
});
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "Photos",
|
|
||||||
Value = "photos"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isForItem)
|
|
||||||
{
|
|
||||||
list.Add(new NameValuePair
|
|
||||||
{
|
|
||||||
Name = "MixedContent",
|
|
||||||
Value = string.Empty
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var val in list)
|
|
||||||
{
|
|
||||||
val.Name = _localizationManager.GetLocalizedString(val.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private SeriesStatus? GetSeriesStatus(BaseItemDto item)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(item.Status))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (SeriesStatus)Enum.Parse(typeof(SeriesStatus), item.Status, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DateTime NormalizeDateTime(DateTime val)
|
||||||
|
{
|
||||||
|
return DateTime.SpecifyKind(val, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<NameValuePair> GetContentTypeOptions(bool isForItem)
|
||||||
|
{
|
||||||
|
var list = new List<NameValuePair>();
|
||||||
|
|
||||||
|
if (isForItem)
|
||||||
|
{
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Inherit",
|
||||||
|
Value = string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Movies",
|
||||||
|
Value = "movies"
|
||||||
|
});
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Music",
|
||||||
|
Value = "music"
|
||||||
|
});
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Shows",
|
||||||
|
Value = "tvshows"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isForItem)
|
||||||
|
{
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Books",
|
||||||
|
Value = "books"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "HomeVideos",
|
||||||
|
Value = "homevideos"
|
||||||
|
});
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "MusicVideos",
|
||||||
|
Value = "musicvideos"
|
||||||
|
});
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "Photos",
|
||||||
|
Value = "photos"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isForItem)
|
||||||
|
{
|
||||||
|
list.Add(new NameValuePair
|
||||||
|
{
|
||||||
|
Name = "MixedContent",
|
||||||
|
Value = string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var val in list)
|
||||||
|
{
|
||||||
|
val.Name = _localizationManager.GetLocalizedString(val.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user