diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml
index 1618237f1a..c28b1bf7f0 100644
--- a/.ci/azure-pipelines-package.yml
+++ b/.ci/azure-pipelines-package.yml
@@ -47,7 +47,7 @@ jobs:
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
- - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
+ - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)" deployment'
displayName: 'Build Dockerfile'
- script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'
diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index fd377df9db..5878028330 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -30,9 +30,9 @@ body:
label: Jellyfin Version
description: What version of Jellyfin are you running?
options:
- - 10.8.0
+ - 10.8.z
+ - 10.8.9
- 10.7.7
- - 10.7.z
- 10.6.4
- Other
validations:
@@ -47,13 +47,15 @@ body:
label: Environment
description: |
Examples:
- - **OS**: [e.g. Debian, Windows]
+ - **OS**: [e.g. Debian 11, Windows 10]
+ - **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
- **Virtualization**: [e.g. Docker, KVM, LXC]
- **Clients**: [Browser, Android, Fire Stick, etc.]
- **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
- - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+ - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
- **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
- **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+ - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
- **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
- **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
- **Base URL**: [e.g. none, yes: /example]
@@ -61,12 +63,14 @@ body:
- **Storage**: [e.g. local, NFS, cloud]
value: |
- OS:
+ - Linux Kernel:
- Virtualization:
- Clients:
- Browser:
- FFmpeg Version:
- Playback Method:
- Hardware Acceleration:
+ - GPU Model:
- Plugins:
- Reverse Proxy:
- Base URL:
@@ -84,8 +88,8 @@ body:
id: ffmpeg-logs
attributes:
label: FFmpeg logs
- description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
- placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+ description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
+ placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
render: shell
- type: textarea
id: browserlogs
diff --git a/.github/workflows/automation.yml b/.github/workflows/automation.yml
index 4b5571c774..47abce02a3 100644
--- a/.github/workflows/automation.yml
+++ b/.github/workflows/automation.yml
@@ -19,6 +19,7 @@ jobs:
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
with:
dirtyLabel: 'merge conflict'
+ commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
repoToken: ${{ secrets.JF_BOT_TOKEN }}
project:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5779ac3cf9..f83b38949c 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+ uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 5d945c001b..178959afc9 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
with:
token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }}
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify as seen
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
- name: Notify as running
id: comment_running
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
exit ${retcode}
- name: Notify with result success
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && success() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
reactions: hooray
- name: Notify with result failure
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ github.event.comment != null && failure() }}
with:
token: ${{ secrets.JF_BOT_TOKEN }}
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 4577ff5251..d3dfd0a6aa 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,18 +14,18 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-head
retention-days: 14
@@ -39,25 +39,27 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+ uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0
- name: Checkout common ancestor
+ env:
+ HEAD_REF: ${{ github.head_ref }}
run: |
git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
- ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
+ ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
- uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+ uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+ uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: openapi-base
retention-days: 14
@@ -76,12 +78,12 @@ jobs:
- openapi-base
steps:
- name: Download openapi-head
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-head
path: openapi-head
- name: Download openapi-base
- uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+ uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
with:
name: openapi-base
path: openapi-base
@@ -103,14 +105,14 @@ jobs:
body="${body//$'\r'/'%0D'}"
echo ::set-output name=body::$body
- name: Find difference comment
- uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
+ uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
direction: last
body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
@@ -125,7 +127,7 @@ jobs:
- name: Edit difference comment (unchanged)
- uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+ uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with:
issue-number: ${{ github.event.pull_request.number }}
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index 7f6fcffed5..c753c1600a 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -1,4 +1,4 @@
-name: Issue Stale Check
+name: Stale Check
on:
schedule:
@@ -7,12 +7,15 @@ on:
permissions:
issues: write
+ pull-requests: write
+
jobs:
- stale:
+ issues:
+ name: Check issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
- - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
+ - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with:
repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120
@@ -28,3 +31,21 @@ jobs:
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
+
+ prs-conflicts:
+ name: Check PRs with merge conflicts
+ runs-on: ubuntu-latest
+ if: ${{ contains(github.repository, 'jellyfin/') }}
+ steps:
+ - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
+ with:
+ repo-token: ${{ secrets.JF_BOT_TOKEN }}
+ operations-per-run: 75
+ # The merge conflict action will remove the label when updated
+ remove-stale-when-updated: false
+ days-before-stale: -1
+ days-before-close: 90
+ days-before-issue-close: -1
+ stale-pr-label: merge conflict
+ close-pr-message: |-
+ This PR has been closed due to having unresolved merge conflicts.
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index b7a317000b..0000000000
--- a/.npmrc
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index ec3c6fd2af..dfb61df0a1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -58,6 +58,7 @@
- [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog)
- [jftuga](https://github.com/jftuga)
+ - [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h)
- [joshuaboniface](https://github.com/joshuaboniface)
- [JustAMan](https://github.com/JustAMan)
@@ -125,6 +126,7 @@
- [SuperSandro2000](https://github.com/SuperSandro2000)
- [tbraeutigam](https://github.com/tbraeutigam)
- [teacupx](https://github.com/teacupx)
+ - [TelepathicWalrus](https://github.com/TelepathicWalrus)
- [Terror-Gene](https://github.com/Terror-Gene)
- [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
- [ThibaultNocchi](https://github.com/ThibaultNocchi)
@@ -162,6 +164,8 @@
- [vgambier](https://github.com/vgambier)
- [MinecraftPlaye](https://github.com/MinecraftPlaye)
- [RealGreenDragon](https://github.com/RealGreenDragon)
+ - [ipitio](https://github.com/ipitio)
+ - [TheTyrius](https://github.com/TheTyrius)
# Emby Contributors
@@ -231,3 +235,4 @@
- [Matthew Jones](https://github.com/matthew-jones-uk)
- [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir)
+ - [JPUC1143](https://github.com/Jpuc1143/)
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000000..c3532467af
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,92 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dockerfile b/Dockerfile
index 304f794631..e51d285e12 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM debian:stable-slim as app
@@ -37,7 +38,7 @@ RUN apt-get update \
&& apt-get update \
&& apt-get install --no-install-recommends --no-install-suggests -y \
mesa-va-drivers \
- jellyfin-ffmpeg \
+ jellyfin-ffmpeg5 \
openssl \
locales \
# Intel VAAPI Tone mapping dependencies:
diff --git a/Dockerfile.arm b/Dockerfile.arm
index bbb84a461c..46a3e9b998 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-arm as qemu
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 5572586ae9..4f9d5e1fdc 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
&& cd jellyfin-web-* \
&& npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
&& mv dist /dist
FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
diff --git a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
index c484dac542..db1190ae7c 100644
--- a/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
+++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
@@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
/// The .
private static IEnumerable GetStateVariables()
{
- var list = new List
+ return new StateVariable[]
{
new StateVariable
{
@@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
SendsEvents = false
}
};
-
- return list;
}
}
}
diff --git a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
index 3edaabb70e..9af28aa7cb 100644
--- a/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
+++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
@@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
/// The .
private static IEnumerable GetStateVariables()
{
- var list = new List
+ return new StateVariable[]
{
new StateVariable
{
@@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
SendsEvents = false
}
};
-
- return list;
}
}
}
diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs
index bea7a5a0da..f668dc829a 100644
--- a/Emby.Dlna/Didl/DidlBuilder.cs
+++ b/Emby.Dlna/Didl/DidlBuilder.cs
@@ -10,6 +10,7 @@ using System.Text;
using System.Xml;
using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
@@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
var types = new[]
{
- PersonType.Director,
- PersonType.Writer,
- PersonType.Producer,
- PersonType.Composer,
- "creator"
+ PersonKind.Director,
+ PersonKind.Writer,
+ PersonKind.Producer,
+ PersonKind.Composer,
+ PersonKind.Creator
};
// Seeing some LG models locking up due content with large lists of people
@@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
foreach (var actor in people)
{
- var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
- ?? PersonType.Actor;
+ var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
+ if (type == PersonKind.Unknown)
+ {
+ type = PersonKind.Actor;
+ }
- AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
+ AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
}
}
diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj
index 60e6dd644d..aca2399644 100644
--- a/Emby.Dlna/Emby.Dlna.csproj
+++ b/Emby.Dlna/Emby.Dlna.csproj
@@ -28,13 +28,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
-
+
+
+
@@ -80,7 +80,7 @@
-
+
diff --git a/Emby.Dlna/Eventing/DlnaEventManager.cs b/Emby.Dlna/Eventing/DlnaEventManager.cs
index c0eacf5d83..ecbbdf9df9 100644
--- a/Emby.Dlna/Eventing/DlnaEventManager.cs
+++ b/Emby.Dlna/Eventing/DlnaEventManager.cs
@@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
try
{
- using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+ using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
}
catch (OperationCanceledException)
diff --git a/Emby.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs
index aab475153b..39cfc2d1d4 100644
--- a/Emby.Dlna/Main/DlnaEntryPoint.cs
+++ b/Emby.Dlna/Main/DlnaEntryPoint.cs
@@ -7,7 +7,6 @@ using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Sockets;
-using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Emby.Dlna.PlayTo;
using Emby.Dlna.Ssdp;
diff --git a/Emby.Dlna/PlayTo/DlnaHttpClient.cs b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
index 75ff542dd8..8b983e9e3d 100644
--- a/Emby.Dlna/PlayTo/DlnaHttpClient.cs
+++ b/Emby.Dlna/PlayTo/DlnaHttpClient.cs
@@ -2,9 +2,11 @@
using System;
using System.Globalization;
+using System.IO;
using System.Net.Http;
using System.Net.Mime;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
@@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
namespace Emby.Dlna.PlayTo
{
- public class DlnaHttpClient
+ ///
+ /// Http client for Dlna PlayTo function.
+ ///
+ public partial class DlnaHttpClient
{
private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory;
@@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
private async Task SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
- using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
+ using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using MemoryStream ms = new MemoryStream();
+ await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
try
{
return await XDocument.LoadAsync(
- stream,
+ ms,
LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
- catch (XmlException ex)
+ catch (XmlException)
{
- _logger.LogError(ex, "Failed to parse response");
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
- }
+ // try correcting the Xml response with common errors
+ ms.Position = 0;
+ using StreamReader sr = new StreamReader(ms);
+ var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
- return null;
+ // find and replace unescaped ampersands (&)
+ xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
+
+ try
+ {
+ // retry reading Xml
+ using var xmlReader = new StringReader(xmlString);
+ return await XDocument.LoadAsync(
+ xmlReader,
+ LoadOptions.None,
+ cancellationToken).ConfigureAwait(false);
+ }
+ catch (XmlException ex)
+ {
+ _logger.LogError(ex, "Failed to parse response");
+ _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+
+ return null;
+ }
}
}
@@ -104,5 +128,12 @@ namespace Emby.Dlna.PlayTo
// Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
}
+
+ ///
+ /// Compile-time generated regular expression for escaping ampersands.
+ ///
+ /// Compiled regular expression.
+ [GeneratedRegex("(&(?![a-z]*;))")]
+ private static partial Regex EscapeAmpersandRegex();
}
}
diff --git a/Emby.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs
index 7b1f942c5a..86db363374 100644
--- a/Emby.Dlna/PlayTo/PlayToController.cs
+++ b/Emby.Dlna/PlayTo/PlayToController.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
IUserDataManager userDataManager,
ILocalizationManager localization,
IMediaSourceManager mediaSourceManager,
- IMediaEncoder mediaEncoder)
+ IMediaEncoder mediaEncoder,
+ Device device)
{
_session = session;
_sessionManager = sessionManager;
@@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
_localization = localization;
_mediaSourceManager = mediaSourceManager;
_mediaEncoder = mediaEncoder;
- }
- public bool IsSessionActive => !_disposed && _device is not null;
-
- public bool SupportsMediaControl => IsSessionActive;
-
- public void Init(Device device)
- {
_device = device;
_device.OnDeviceUnavailable = OnDeviceUnavailable;
_device.PlaybackStart += OnDevicePlaybackStart;
@@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
_deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
}
+ public bool IsSessionActive => !_disposed;
+
+ public bool SupportsMediaControl => IsSessionActive;
+
/*
* Send a message to the DLNA device to notify what is the next track in the playlist.
*/
@@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
}
}
- private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs e)
+ private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs e)
{
var info = e.Argument;
if (!_disposed
- && info.Headers.TryGetValue("USN", out string usn)
+ && info.Headers.TryGetValue("USN", out string? usn)
&& usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
&& (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
- || (info.Headers.TryGetValue("NT", out string nt)
+ || (info.Headers.TryGetValue("NT", out string? nt)
&& nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
{
OnDeviceUnavailable();
}
}
- private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
+ private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
{
if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
{
@@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
+ private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
{
if (_disposed)
{
@@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
+ private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
{
if (_disposed)
{
@@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
}
}
- private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
+ private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
{
if (_disposed)
{
@@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
private PlaylistItem CreatePlaylistItem(
BaseItem item,
- User user,
+ User? user,
long startPostionTicks,
- string mediaSourceId,
+ string? mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
@@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
return playlistItem;
}
- private string GetDlnaHeaders(PlaylistItem item)
+ private string? GetDlnaHeaders(PlaylistItem item)
{
var profile = item.Profile;
var streamInfo = item.StreamInfo;
@@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+ private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
{
if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
{
@@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
_device.MediaChanged -= OnDeviceMediaChanged;
_deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
_device.OnDeviceUnavailable = null;
- _device = null;
_disposed = true;
}
@@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
case GeneralCommandType.ToggleMute:
return _device.ToggleMute(cancellationToken);
case GeneralCommandType.SetAudioStreamIndex:
- if (command.Arguments.TryGetValue("Index", out string index))
+ if (command.Arguments.TryGetValue("Index", out string? index))
{
if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
{
@@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
case GeneralCommandType.SetVolume:
- if (command.Arguments.TryGetValue("Volume", out string vol))
+ if (command.Arguments.TryGetValue("Volume", out string? vol))
{
if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
{
@@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
throw new ObjectDisposedException(GetType().Name);
}
- if (_device is null)
+ return name switch
{
- return Task.CompletedTask;
- }
-
- if (name == SessionMessageType.Play)
- {
- return SendPlayCommand(data as PlayRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.Playstate)
- {
- return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
- }
-
- if (name == SessionMessageType.GeneralCommand)
- {
- return SendGeneralCommand(data as GeneralCommand, cancellationToken);
- }
-
- // Not supported or needed right now
- return Task.CompletedTask;
+ SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
+ SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
+ SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
+ _ => Task.CompletedTask // Not supported or needed right now
+ };
}
private class StreamParams
{
- private MediaSourceInfo _mediaSource;
- private IMediaSourceManager _mediaSourceManager;
+ private MediaSourceInfo? _mediaSource;
+ private IMediaSourceManager? _mediaSourceManager;
public Guid ItemId { get; set; }
@@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
public int? SubtitleStreamIndex { get; set; }
- public string DeviceProfileId { get; set; }
+ public string? DeviceProfileId { get; set; }
- public string DeviceId { get; set; }
+ public string? DeviceId { get; set; }
- public string MediaSourceId { get; set; }
+ public string? MediaSourceId { get; set; }
- public string LiveStreamId { get; set; }
+ public string? LiveStreamId { get; set; }
- public BaseItem Item { get; set; }
+ public BaseItem? Item { get; set; }
- public async Task GetMediaSource(CancellationToken cancellationToken)
+ public async Task GetMediaSource(CancellationToken cancellationToken)
{
if (_mediaSource is not null)
{
@@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
{
var part = parts[i];
- if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
- string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
{
if (Guid.TryParse(parts[i + 1], out var result))
{
diff --git a/Emby.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs
index f4a9a90af4..b469c9cb06 100644
--- a/Emby.Dlna/PlayTo/PlayToManager.cs
+++ b/Emby.Dlna/PlayTo/PlayToManager.cs
@@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
_userDataManager,
_localization,
_mediaSourceManager,
- _mediaEncoder);
+ _mediaEncoder,
+ device);
sessionInfo.AddController(controller);
- controller.Init(device);
-
var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
_dlnaManager.GetDefaultProfile();
diff --git a/Emby.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs
index c463727329..6b2096d9dc 100644
--- a/Emby.Dlna/PlayTo/TransportCommands.cs
+++ b/Emby.Dlna/PlayTo/TransportCommands.cs
@@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
- public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
+ public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
{
var stateString = string.Empty;
@@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
}
}
- return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
+ return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
- public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary dictionary)
+ public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary dictionary)
{
var stateString = string.Empty;
@@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
{
stateString += BuildArgumentXml(arg, "0");
}
- else if (dictionary.ContainsKey(arg.Name))
+ else if (dictionary.TryGetValue(arg.Name, out var argValue))
{
- stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
+ stateString += BuildArgumentXml(arg, argValue);
}
else
{
@@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo
}
}
- return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
+ return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
}
private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")
diff --git a/Emby.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
index d00df781d6..69ef6f6456 100644
--- a/Emby.Dlna/Server/DescriptionXmlBuilder.cs
+++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs
@@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
}
}
- private string GetFriendlyName()
+ internal string GetFriendlyName()
{
if (string.IsNullOrEmpty(_profile.FriendlyName))
{
- return "Jellyfin - " + _serverName;
+ return _serverName;
+ }
+
+ if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
+ {
+ return _profile.FriendlyName;
}
var characterList = new List();
@@ -164,13 +169,18 @@ namespace Emby.Dlna.Server
}
}
- var characters = characterList.ToArray();
+ var serverName = string.Create(
+ characterList.Count,
+ characterList,
+ (dest, source) =>
+ {
+ for (int i = 0; i < dest.Length; i++)
+ {
+ dest[i] = source[i];
+ }
+ });
- var serverName = new string(characters);
-
- var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
-
- return name ?? string.Empty;
+ return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
}
private void AppendIconList(StringBuilder builder)
diff --git a/Emby.Naming/Audio/AlbumParser.cs b/Emby.Naming/Audio/AlbumParser.cs
index bbfdccc902..86a5641531 100644
--- a/Emby.Naming/Audio/AlbumParser.cs
+++ b/Emby.Naming/Audio/AlbumParser.cs
@@ -3,6 +3,7 @@ using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
namespace Emby.Naming.Audio
{
@@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
var tmp = trimmedFilename.Slice(prefix.Length).Trim();
- int index = tmp.IndexOf(' ');
- if (index != -1)
- {
- tmp = tmp.Slice(0, index);
- }
-
- if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
+ if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
{
return true;
}
diff --git a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
index 7b4429ab15..75fdedfeab 100644
--- a/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookFilePathParser.cs
@@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
var fileName = Path.GetFileNameWithoutExtension(path);
foreach (var expression in _options.AudioBookPartsExpressions)
{
- var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName);
+ var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (!result.ChapterNumber.HasValue)
@@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["chapter"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.ChapterNumber = intValue;
}
@@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["part"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.PartNumber = intValue;
}
diff --git a/Emby.Naming/AudioBook/AudioBookListResolver.cs b/Emby.Naming/AudioBook/AudioBookListResolver.cs
index bdae20b6b2..ca304102fd 100644
--- a/Emby.Naming/AudioBook/AudioBookListResolver.cs
+++ b/Emby.Naming/AudioBook/AudioBookListResolver.cs
@@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
{
if (group.Count() > 1 || haveChaptersOrPages)
{
- var ex = new List();
- var alt = new List();
+ List? ex = null;
+ List? alt = null;
foreach (var audioFile in group)
{
- var name = Path.GetFileNameWithoutExtension(audioFile.Path);
- if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
- name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
- name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+ var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
+ if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
+ || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
+ || name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
{
- alt.Add(audioFile);
+ (alt ??= new()).Add(audioFile);
}
else
{
- ex.Add(audioFile);
+ (ex ??= new()).Add(audioFile);
}
}
- if (ex.Count > 0)
+ if (ex is not null)
{
var extra = ex
.OrderBy(x => x.Container)
@@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
extras.AddRange(extra);
}
- if (alt.Count > 0)
+ if (alt is not null)
{
var alternatives = alt
.OrderBy(x => x.Container)
diff --git a/Emby.Naming/AudioBook/AudioBookNameParser.cs b/Emby.Naming/AudioBook/AudioBookNameParser.cs
index 97b34199e0..5ea649dbf7 100644
--- a/Emby.Naming/AudioBook/AudioBookNameParser.cs
+++ b/Emby.Naming/AudioBook/AudioBookNameParser.cs
@@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
AudioBookNameParserResult result = default;
foreach (var expression in _options.AudioBookNamesExpressions)
{
- var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+ var match = Regex.Match(name, expression, RegexOptions.IgnoreCase);
if (match.Success)
{
if (result.Name is null)
@@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
var value = match.Groups["year"];
if (value.Success)
{
- if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+ if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
{
result.Year = intValue;
}
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 54f62a1570..a069da1022 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -141,8 +141,7 @@ namespace Emby.Naming.Common
VideoFileStackingRules = new[]
{
new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
- new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false),
- new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?[a-d])(?:\.[^.]+)?$", false)
+ new FileStackRule(@"^(?.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?cd|dvd|part|pt|dis[ck])[ _.-]*(?[a-d])[\)\]]?(?:\.[^.]+)?$", false)
};
CleanDateTimes = new[]
@@ -157,7 +156,8 @@ namespace Emby.Naming.Common
@"^(?.+?)(\[.*\])",
@"^\s*(?.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
@"^\s*\[[^\]]+\](?!\.\w+$)\s*(?.+)",
- @"^\s*(?.+?)\s+-\s+[0-9]+\s*$"
+ @"^\s*(?.+?)\s+-\s+[0-9]+\s*$",
+ @"^\s*(?.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
};
SubtitleFileExtensions = new[]
@@ -270,7 +270,6 @@ namespace Emby.Naming.Common
".sfx",
".shn",
".sid",
- ".spc",
".stm",
".strm",
".ult",
@@ -338,7 +337,15 @@ namespace Emby.Naming.Common
}
},
- // This isn't a Kodi naming rule, but the expression below causes false positives,
+ // This isn't a Kodi naming rule, but the expression below causes false episode numbers for
+ // Title Season X Episode X naming schemes.
+ // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
+ new EpisodeExpression(@".*[\\\/]((?[^\\/]+?)\s)?[Ss](?:eason)?\s*(?[0-9]+)\s+[Ee](?:pisode)?\s*(?[0-9]+).*$")
+ {
+ IsNamed = true
+ },
+
+ // Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?[\w\s]+?)\s(?[0-9]{1,4})(-(?[0-9]{2,4}))*[^\\\/x]*$")
@@ -453,16 +460,6 @@ namespace Emby.Naming.Common
},
};
- EpisodeWithoutSeasonExpressions = new[]
- {
- @"[/\._ \-]()([0-9]+)(-[0-9]+)?"
- };
-
- EpisodeMultiPartExpressions = new[]
- {
- @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
- };
-
VideoExtraRules = new[]
{
new ExtraRule(
@@ -797,16 +794,6 @@ namespace Emby.Naming.Common
///
public EpisodeExpression[] EpisodeExpressions { get; set; }
- ///
- /// Gets or sets list of raw episode without season regular expressions strings.
- ///
- public string[] EpisodeWithoutSeasonExpressions { get; set; }
-
- ///
- /// Gets or sets list of raw multi-part episodes regular expressions strings.
- ///
- public string[] EpisodeMultiPartExpressions { get; set; }
-
///
/// Gets or sets list of video file extensions.
///
@@ -877,16 +864,6 @@ namespace Emby.Naming.Common
///
public Regex[] CleanStringRegexes { get; private set; } = Array.Empty();
- ///
- /// Gets list of episode without season regular expressions.
- ///
- public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty();
-
- ///
- /// Gets list of multi-part episode regular expressions.
- ///
- public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty();
-
///
/// Compiles raw regex strings into regexes.
///
@@ -894,8 +871,6 @@ namespace Emby.Naming.Common
{
CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
- EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
- EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
}
private Regex Compile(string exp)
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 3106e22465..f3973dad95 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -42,18 +42,18 @@
-
+
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
-
+
+
+
diff --git a/Emby.Naming/TV/EpisodePathParser.cs b/Emby.Naming/TV/EpisodePathParser.cs
index d706be2802..8cd5a126e0 100644
--- a/Emby.Naming/TV/EpisodePathParser.cs
+++ b/Emby.Naming/TV/EpisodePathParser.cs
@@ -113,7 +113,7 @@ namespace Emby.Naming.TV
if (expression.DateTimeFormats.Length > 0)
{
if (DateTime.TryParseExact(
- match.Groups[0].Value,
+ match.Groups[0].ValueSpan,
expression.DateTimeFormats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
result.Success = true;
}
}
- else if (DateTime.TryParse(match.Groups[0].Value, out date))
+ else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
{
result.Year = date.Year;
result.Month = date.Month;
@@ -138,12 +138,12 @@ namespace Emby.Naming.TV
}
else if (expression.IsNamed)
{
- if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
@@ -158,7 +158,7 @@ namespace Emby.Naming.TV
if (nextIndex >= name.Length
|| !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
{
- if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EndingEpisodeNumber = num;
}
@@ -170,12 +170,12 @@ namespace Emby.Naming.TV
}
else
{
- if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+ if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
result.SeasonNumber = num;
}
- if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+ if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
{
result.EpisodeNumber = num;
}
diff --git a/Emby.Naming/TV/SeriesResolver.cs b/Emby.Naming/TV/SeriesResolver.cs
index 156a03c9ed..307a840964 100644
--- a/Emby.Naming/TV/SeriesResolver.cs
+++ b/Emby.Naming/TV/SeriesResolver.cs
@@ -14,7 +14,7 @@ namespace Emby.Naming.TV
/// Used for removing separators between words, i.e turns "The_show" into "The show" while
/// preserving namings like "S.H.O.W".
///
- private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))");
+ private static readonly Regex _seriesNameRegex = new Regex(@"((?[^\._]{2,})[\._]*)|([\._](?[^\._]{2,}))", RegexOptions.Compiled);
///
/// Resolve information about series from path.
diff --git a/Emby.Naming/Video/CleanDateTimeParser.cs b/Emby.Naming/Video/CleanDateTimeParser.cs
index 0ee633dcc6..9a6c6e978b 100644
--- a/Emby.Naming/Video/CleanDateTimeParser.cs
+++ b/Emby.Naming/Video/CleanDateTimeParser.cs
@@ -43,7 +43,7 @@ namespace Emby.Naming.Video
&& match.Groups.Count == 5
&& match.Groups[1].Success
&& match.Groups[2].Success
- && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
+ && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
{
result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
return true;
diff --git a/Emby.Naming/Video/ExtraRuleResolver.cs b/Emby.Naming/Video/ExtraRuleResolver.cs
index 21d0da3642..3219472eff 100644
--- a/Emby.Naming/Video/ExtraRuleResolver.cs
+++ b/Emby.Naming/Video/ExtraRuleResolver.cs
@@ -56,7 +56,7 @@ namespace Emby.Naming.Video
}
else if (rule.RuleType == ExtraRuleType.Regex)
{
- var filename = Path.GetFileName(path);
+ var filename = Path.GetFileName(path.AsSpan());
var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
diff --git a/Emby.Naming/Video/FileStackRule.cs b/Emby.Naming/Video/FileStackRule.cs
index 76b487f428..be0f79d33a 100644
--- a/Emby.Naming/Video/FileStackRule.cs
+++ b/Emby.Naming/Video/FileStackRule.cs
@@ -17,7 +17,7 @@ public class FileStackRule
/// Whether the file stack rule uses numerical or alphabetical numbering.
public FileStackRule(string token, bool isNumerical)
{
- _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+ _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
IsNumerical = isNumerical;
}
diff --git a/Emby.Naming/Video/VideoListResolver.cs b/Emby.Naming/Video/VideoListResolver.cs
index 8048320400..6209cd46f4 100644
--- a/Emby.Naming/Video/VideoListResolver.cs
+++ b/Emby.Naming/Video/VideoListResolver.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Emby.Naming.Common;
+using Jellyfin.Extensions;
using MediaBrowser.Model.IO;
namespace Emby.Naming.Video
@@ -13,6 +14,8 @@ namespace Emby.Naming.Video
///
public static class VideoListResolver
{
+ private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
///
/// Resolves alternative versions and extras from list of video files.
///
@@ -106,6 +109,7 @@ namespace Emby.Naming.Video
}
// Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+ VideoInfo? primary = null;
for (var i = 0; i < videos.Count; i++)
{
var video = videos[i];
@@ -114,29 +118,43 @@ namespace Emby.Naming.Video
continue;
}
- if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
+ if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
{
return videos;
}
+
+ if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
+ {
+ primary = video;
+ }
}
- // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
- videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
+ if (videos.Count > 1)
+ {
+ var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+ videos.Clear();
+ foreach (var group in groups)
+ {
+ if (group.Key)
+ {
+ videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+ }
+ else
+ {
+ videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+ }
+ }
+ }
+
+ primary ??= videos[0];
+ videos.Remove(primary);
var list = new List
{
- videos[0]
+ primary
};
- var alternateVersionsLen = videos.Count - 1;
- var alternateVersions = new VideoFileInfo[alternateVersionsLen];
- for (int i = 0; i < alternateVersionsLen; i++)
- {
- var video = videos[i + 1];
- alternateVersions[i] = video.Files[0];
- }
-
- list[0].AlternateVersions = alternateVersions;
+ list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
list[0].Name = folderName.ToString();
return list;
@@ -161,9 +179,8 @@ namespace Emby.Naming.Video
return true;
}
- private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, string testFilePath, NamingOptions namingOptions)
+ private static bool IsEligibleForMultiVersion(ReadOnlySpan folderName, ReadOnlySpan testFilename, NamingOptions namingOptions)
{
- var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
{
return false;
@@ -176,16 +193,15 @@ namespace Emby.Naming.Video
}
// There are no span overloads for regex unfortunately
- var tmpTestFilename = testFilename.ToString();
- if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+ if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
{
- tmpTestFilename = cleanName.Trim();
+ testFilename = cleanName.AsSpan().Trim();
}
// The CleanStringParser should have removed common keywords etc.
- return string.IsNullOrEmpty(tmpTestFilename)
+ return testFilename.IsEmpty
|| testFilename[0] == '-'
- || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+ || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
}
}
}
diff --git a/Emby.Naming/Video/VideoResolver.cs b/Emby.Naming/Video/VideoResolver.cs
index 858e9dd2f5..db5bfdbf94 100644
--- a/Emby.Naming/Video/VideoResolver.cs
+++ b/Emby.Naming/Video/VideoResolver.cs
@@ -87,8 +87,7 @@ namespace Emby.Naming.Video
name = cleanDateTimeResult.Name;
year = cleanDateTimeResult.Year;
- if (extraResult.ExtraType is null
- && TryCleanString(name, namingOptions, out var newName))
+ if (TryCleanString(name, namingOptions, out var newName))
{
name = newName;
}
diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj
index ae6bc2db1f..0f97a06867 100644
--- a/Emby.Photos/Emby.Photos.csproj
+++ b/Emby.Photos/Emby.Photos.csproj
@@ -15,7 +15,7 @@
-
+
@@ -26,13 +26,13 @@
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
-
+
+
+
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index 985a127d50..a4deeddb78 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
private ConfigurationStore[] _configurationStores = Array.Empty();
private IConfigurationFactory[] _configurationFactories = Array.Empty();
- ///
- /// The _configuration loaded.
- ///
- private bool _configurationLoaded;
-
///
/// The _configuration.
///
- private BaseApplicationConfiguration _configuration;
+ private BaseApplicationConfiguration? _configuration;
///
/// Initializes a new instance of the class.
@@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
///
/// Occurs when [configuration updated].
///
- public event EventHandler ConfigurationUpdated;
+ public event EventHandler? ConfigurationUpdated;
///
/// Occurs when [configuration updating].
///
- public event EventHandler NamedConfigurationUpdating;
+ public event EventHandler? NamedConfigurationUpdating;
///
/// Occurs when [named configuration updated].
///
- public event EventHandler NamedConfigurationUpdated;
+ public event EventHandler? NamedConfigurationUpdated;
///
/// Gets the type of the configuration.
@@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
{
get
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
lock (_configurationSyncLock)
{
- if (_configurationLoaded)
+ if (_configuration is not null)
{
return _configuration;
}
- _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
-
- _configurationLoaded = true;
-
- return _configuration;
+ return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
}
}
protected set
{
_configuration = value;
-
- _configurationLoaded = value is not null;
}
}
@@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
Logger.LogInformation("Saving system configuration");
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)
{
@@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
private object LoadConfiguration(string path, Type configurationType)
{
- if (!File.Exists(path))
- {
- return Activator.CreateInstance(configurationType);
- }
-
try
{
- return XmlSerializer.DeserializeFromFile(configurationType, path);
+ if (File.Exists(path))
+ {
+ return XmlSerializer.DeserializeFromFile(configurationType, path);
+ }
}
- catch (IOException)
- {
- return Activator.CreateInstance(configurationType);
- }
- catch (Exception ex)
+ catch (Exception ex) when (ex is not IOException)
{
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.");
}
///
@@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
_configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
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)
{
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 07b0807b72..7969577bc0 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -11,7 +11,6 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -81,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.System;
@@ -113,15 +114,11 @@ namespace Emby.Server.Implementations
///
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
{
- ///
- /// The environment variable prefixes to log at server startup.
- ///
- private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
-
///
/// The disposable parts.
///
private readonly ConcurrentDictionary _disposableParts = new();
+ private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
@@ -130,7 +127,6 @@ namespace Emby.Server.Implementations
private readonly IPluginManager _pluginManager;
private List _creatingInstances;
- private IMediaEncoder _mediaEncoder;
private ISessionManager _sessionManager;
///
@@ -139,8 +135,6 @@ namespace Emby.Server.Implementations
/// All concrete types.
private Type[] _allConcreteTypes;
- private DeviceId _deviceId;
-
private bool _disposed = false;
///
@@ -164,6 +158,7 @@ namespace Emby.Server.Implementations
Logger = LoggerFactory.CreateLogger();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+ _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -191,23 +186,9 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; }
- public virtual bool CanLaunchWebBrowser
- {
- get
- {
- if (!Environment.UserInteractive)
- {
- return false;
- }
-
- if (_startupOptions.IsService)
- {
- return false;
- }
-
- return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
- }
- }
+ public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
+ && !_startupOptions.IsService
+ && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
///
/// Gets the singleton instance.
@@ -284,15 +265,7 @@ namespace Emby.Server.Implementations
/// The application name.
public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
- public string SystemId
- {
- get
- {
- _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
-
- return _deviceId.Value;
- }
- }
+ public string SystemId => _deviceId.Value;
///
public string Name => ApplicationProductName;
@@ -445,7 +418,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
- _mediaEncoder.SetFFmpegPath();
+ Resolve().SetFFmpegPath();
Logger.LogInformation("ServerId: {ServerId}", SystemId);
@@ -558,6 +531,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -652,50 +627,19 @@ namespace Emby.Server.Implementations
}
}
+ ((SqliteItemRepository)Resolve()).Initialize();
+ ((SqliteUserDataRepository)Resolve()).Initialize();
+
var localizationManager = (LocalizationManager)Resolve();
await localizationManager.LoadAll().ConfigureAwait(false);
- _mediaEncoder = Resolve();
_sessionManager = Resolve();
SetStaticProperties();
- var userDataRepo = (SqliteUserDataRepository)Resolve();
- ((SqliteItemRepository)Resolve()).Initialize(userDataRepo, Resolve());
-
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
/// Instance of the interface.
/// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
public SyncPlayAccessHandler(
ISyncPlayManager syncPlayManager,
- IUserManager userManager,
- INetworkManager networkManager,
- IHttpContextAccessor httpContextAccessor)
- : base(userManager, networkManager, httpContextAccessor)
+ IUserManager userManager)
{
_syncPlayManager = syncPlayManager;
_userManager = userManager;
@@ -39,27 +32,20 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
///
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, SyncPlayAccessRequirement requirement)
{
- if (!ValidateClaims(context.User))
- {
- context.Fail();
- return Task.CompletedTask;
- }
-
var userId = context.User.GetUserId();
var user = _userManager.GetUserById(userId);
+ if (user is null)
+ {
+ throw new ResourceNotFoundException();
+ }
if (requirement.RequiredAccess == SyncPlayAccessRequirementType.HasAccess)
{
- if (user.SyncPlayAccess == SyncPlayUserAccessType.CreateAndJoinGroups
- || user.SyncPlayAccess == SyncPlayUserAccessType.JoinGroups
+ if (user.SyncPlayAccess is SyncPlayUserAccessType.CreateAndJoinGroups or SyncPlayUserAccessType.JoinGroups
|| _syncPlayManager.IsUserActive(userId))
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.CreateGroup)
{
@@ -67,10 +53,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.JoinGroup)
{
@@ -79,10 +61,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
}
else if (requirement.RequiredAccess == SyncPlayAccessRequirementType.IsInGroup)
{
@@ -90,14 +68,6 @@ namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
context.Succeed(requirement);
}
- else
- {
- context.Fail();
- }
- }
- else
- {
- context.Fail();
}
return Task.CompletedTask;
diff --git a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
index 6fab4c0ad8..220b223b39 100644
--- a/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
+++ b/Jellyfin.Api/Auth/SyncPlayAccessPolicy/SyncPlayAccessRequirement.cs
@@ -1,12 +1,12 @@
-using Jellyfin.Data.Enums;
-using Microsoft.AspNetCore.Authorization;
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
namespace Jellyfin.Api.Auth.SyncPlayAccessPolicy
{
///
/// The default authorization requirement.
///
- public class SyncPlayAccessRequirement : IAuthorizationRequirement
+ public class SyncPlayAccessRequirement : DefaultAuthorizationRequirement
{
///
/// Initializes a new instance of the class.
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
new file mode 100644
index 0000000000..e72bec46fd
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionHandler.cs
@@ -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
+{
+ ///
+ /// User permission authorization handler.
+ ///
+ public class UserPermissionHandler : AuthorizationHandler
+ {
+ private readonly IUserManager _userManager;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Instance of the interface.
+ public UserPermissionHandler(IUserManager userManager)
+ {
+ _userManager = userManager;
+ }
+
+ ///
+ 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;
+ }
+ }
+}
diff --git a/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
new file mode 100644
index 0000000000..4694556eb7
--- /dev/null
+++ b/Jellyfin.Api/Auth/UserPermissionPolicy/UserPermissionRequirement.cs
@@ -0,0 +1,26 @@
+using Jellyfin.Api.Auth.DefaultAuthorizationPolicy;
+using Jellyfin.Data.Enums;
+
+namespace Jellyfin.Api.Auth.UserPermissionPolicy
+{
+ ///
+ /// The user permission requirement.
+ ///
+ public class UserPermissionRequirement : DefaultAuthorizationRequirement
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The required .
+ /// Whether to validate the user's parental schedule.
+ public UserPermissionRequirement(PermissionKind requiredPermission, bool validateParentalSchedule = true) : base(validateParentalSchedule)
+ {
+ RequiredPermission = requiredPermission;
+ }
+
+ ///
+ /// Gets the required user permission.
+ ///
+ public PermissionKind RequiredPermission { get; }
+ }
+}
diff --git a/Jellyfin.Api/BaseJellyfinApiController.cs b/Jellyfin.Api/BaseJellyfinApiController.cs
index e327831fe7..5b4bd0adb0 100644
--- a/Jellyfin.Api/BaseJellyfinApiController.cs
+++ b/Jellyfin.Api/BaseJellyfinApiController.cs
@@ -4,35 +4,34 @@ using Jellyfin.Api.Results;
using Jellyfin.Extensions.Json;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api
+namespace Jellyfin.Api;
+
+///
+/// Base api controller for the API setting a default route.
+///
+[ApiController]
+[Route("[controller]")]
+[Produces(
+ MediaTypeNames.Application.Json,
+ JsonDefaults.CamelCaseMediaType,
+ JsonDefaults.PascalCaseMediaType)]
+public class BaseJellyfinApiController : ControllerBase
{
///
- /// Base api controller for the API setting a default route.
+ /// Create a new .
///
- [ApiController]
- [Route("[controller]")]
- [Produces(
- MediaTypeNames.Application.Json,
- JsonDefaults.CamelCaseMediaType,
- JsonDefaults.PascalCaseMediaType)]
- public class BaseJellyfinApiController : ControllerBase
- {
- ///
- /// Create a new .
- ///
- /// The value to return.
- /// The type to return.
- /// The .
- protected ActionResult> Ok(IEnumerable? value)
- => new OkResult?>(value);
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult> Ok(IEnumerable? value)
+ => new OkResult?>(value);
- ///
- /// Create a new .
- ///
- /// The value to return.
- /// The type to return.
- /// The .
- protected ActionResult Ok(T value)
- => new OkResult(value);
- }
+ ///
+ /// Create a new .
+ ///
+ /// The value to return.
+ /// The type to return.
+ /// The .
+ protected ActionResult Ok(T value)
+ => new OkResult(value);
}
diff --git a/Jellyfin.Api/Constants/AuthenticationSchemes.cs b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
index bac3379e71..d5c2253e4a 100644
--- a/Jellyfin.Api/Constants/AuthenticationSchemes.cs
+++ b/Jellyfin.Api/Constants/AuthenticationSchemes.cs
@@ -1,13 +1,12 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+///
+/// Authentication schemes for user authentication in the API.
+///
+public static class AuthenticationSchemes
{
///
- /// Authentication schemes for user authentication in the API.
+ /// Scheme name for the custom legacy authentication.
///
- public static class AuthenticationSchemes
- {
- ///
- /// Scheme name for the custom legacy authentication.
- ///
- public const string CustomAuthentication = "CustomAuthentication";
- }
+ public const string CustomAuthentication = "CustomAuthentication";
}
diff --git a/Jellyfin.Api/Constants/InternalClaimTypes.cs b/Jellyfin.Api/Constants/InternalClaimTypes.cs
index 8323312e51..73c4acb882 100644
--- a/Jellyfin.Api/Constants/InternalClaimTypes.cs
+++ b/Jellyfin.Api/Constants/InternalClaimTypes.cs
@@ -1,43 +1,42 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+///
+/// Internal claim types for authorization.
+///
+public static class InternalClaimTypes
{
///
- /// Internal claim types for authorization.
+ /// User Id.
///
- public static class InternalClaimTypes
- {
- ///
- /// User Id.
- ///
- public const string UserId = "Jellyfin-UserId";
+ public const string UserId = "Jellyfin-UserId";
- ///
- /// Device Id.
- ///
- public const string DeviceId = "Jellyfin-DeviceId";
+ ///
+ /// Device Id.
+ ///
+ public const string DeviceId = "Jellyfin-DeviceId";
- ///
- /// Device.
- ///
- public const string Device = "Jellyfin-Device";
+ ///
+ /// Device.
+ ///
+ public const string Device = "Jellyfin-Device";
- ///
- /// Client.
- ///
- public const string Client = "Jellyfin-Client";
+ ///
+ /// Client.
+ ///
+ public const string Client = "Jellyfin-Client";
- ///
- /// Version.
- ///
- public const string Version = "Jellyfin-Version";
+ ///
+ /// Version.
+ ///
+ public const string Version = "Jellyfin-Version";
- ///
- /// Token.
- ///
- public const string Token = "Jellyfin-Token";
+ ///
+ /// Token.
+ ///
+ public const string Token = "Jellyfin-Token";
- ///
- /// Is Api Key.
- ///
- public const string IsApiKey = "Jellyfin-IsApiKey";
- }
+ ///
+ /// Is Api Key.
+ ///
+ public const string IsApiKey = "Jellyfin-IsApiKey";
}
diff --git a/Jellyfin.Api/Constants/Policies.cs b/Jellyfin.Api/Constants/Policies.cs
index a72eeea284..53841b0c44 100644
--- a/Jellyfin.Api/Constants/Policies.cs
+++ b/Jellyfin.Api/Constants/Policies.cs
@@ -1,78 +1,87 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+///
+/// Policies for the API authorization.
+///
+public static class Policies
{
///
- /// Policies for the API authorization.
+ /// Policy name for requiring first time setup or elevated privileges.
///
- public static class Policies
- {
- ///
- /// Policy name for default authorization.
- ///
- public const string DefaultAuthorization = "DefaultAuthorization";
+ public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
- ///
- /// Policy name for requiring first time setup or elevated privileges.
- ///
- public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
+ ///
+ /// Policy name for requiring elevated privileges.
+ ///
+ public const string RequiresElevation = "RequiresElevation";
- ///
- /// Policy name for requiring elevated privileges.
- ///
- public const string RequiresElevation = "RequiresElevation";
+ ///
+ /// Policy name for allowing local access only.
+ ///
+ public const string LocalAccessOnly = "LocalAccessOnly";
- ///
- /// Policy name for allowing local access only.
- ///
- public const string LocalAccessOnly = "LocalAccessOnly";
+ ///
+ /// Policy name for escaping schedule controls.
+ ///
+ public const string IgnoreParentalControl = "IgnoreParentalControl";
- ///
- /// Policy name for escaping schedule controls.
- ///
- public const string IgnoreParentalControl = "IgnoreParentalControl";
+ ///
+ /// Policy name for requiring download permission.
+ ///
+ public const string Download = "Download";
- ///
- /// Policy name for requiring download permission.
- ///
- public const string Download = "Download";
+ ///
+ /// Policy name for requiring first time setup or default permissions.
+ ///
+ public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
- ///
- /// Policy name for requiring first time setup or default permissions.
- ///
- public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
+ ///
+ /// Policy name for requiring local access or elevated privileges.
+ ///
+ public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
- ///
- /// Policy name for requiring local access or elevated privileges.
- ///
- public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
+ ///
+ /// Policy name for requiring (anonymous) LAN access.
+ ///
+ public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
- ///
- /// Policy name for requiring (anonymous) LAN access.
- ///
- public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
+ ///
+ /// Policy name for escaping schedule controls or requiring first time setup.
+ ///
+ public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
- ///
- /// Policy name for escaping schedule controls or requiring first time setup.
- ///
- public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
+ ///
+ /// Policy name for accessing SyncPlay.
+ ///
+ public const string SyncPlayHasAccess = "SyncPlayHasAccess";
- ///
- /// Policy name for accessing SyncPlay.
- ///
- public const string SyncPlayHasAccess = "SyncPlayHasAccess";
+ ///
+ /// Policy name for creating a SyncPlay group.
+ ///
+ public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
- ///
- /// Policy name for creating a SyncPlay group.
- ///
- public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
+ ///
+ /// Policy name for joining a SyncPlay group.
+ ///
+ public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
- ///
- /// Policy name for joining a SyncPlay group.
- ///
- public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
+ ///
+ /// Policy name for accessing a SyncPlay group.
+ ///
+ public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
- ///
- /// Policy name for accessing a SyncPlay group.
- ///
- public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
- }
+ ///
+ /// Policy name for accessing collection management.
+ ///
+ public const string CollectionManagement = "CollectionManagement";
+
+ ///
+ /// Policy name for accessing LiveTV.
+ ///
+ public const string LiveTvAccess = "LiveTvAccess";
+
+ ///
+ /// Policy name for managing LiveTV.
+ ///
+ public const string LiveTvManagement = "LiveTvManagement";
}
diff --git a/Jellyfin.Api/Constants/UserRoles.cs b/Jellyfin.Api/Constants/UserRoles.cs
index d9a536e7d7..41c7b7cd0f 100644
--- a/Jellyfin.Api/Constants/UserRoles.cs
+++ b/Jellyfin.Api/Constants/UserRoles.cs
@@ -1,23 +1,22 @@
-namespace Jellyfin.Api.Constants
+namespace Jellyfin.Api.Constants;
+
+///
+/// Constants for user roles used in the authentication and authorization for the API.
+///
+public static class UserRoles
{
///
- /// Constants for user roles used in the authentication and authorization for the API.
+ /// Guest user.
///
- public static class UserRoles
- {
- ///
- /// Guest user.
- ///
- public const string Guest = "Guest";
+ public const string Guest = "Guest";
- ///
- /// Regular user with no special privileges.
- ///
- public const string User = "User";
+ ///
+ /// Regular user with no special privileges.
+ ///
+ public const string User = "User";
- ///
- /// Administrator user with elevated privileges.
- ///
- public const string Administrator = "Administrator";
- }
+ ///
+ /// Administrator user with elevated privileges.
+ ///
+ public const string Administrator = "Administrator";
}
diff --git a/Jellyfin.Api/Controllers/ActivityLogController.cs b/Jellyfin.Api/Controllers/ActivityLogController.cs
index ae45f647f7..c3d02976eb 100644
--- a/Jellyfin.Api/Controllers/ActivityLogController.cs
+++ b/Jellyfin.Api/Controllers/ActivityLogController.cs
@@ -8,50 +8,49 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Activity log controller.
+///
+[Route("System/ActivityLog")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class ActivityLogController : BaseJellyfinApiController
{
+ private readonly IActivityManager _activityManager;
+
///
- /// Activity log controller.
+ /// Initializes a new instance of the class.
///
- [Route("System/ActivityLog")]
- [Authorize(Policy = Policies.RequiresElevation)]
- public class ActivityLogController : BaseJellyfinApiController
+ /// Instance of interface.
+ public ActivityLogController(IActivityManager activityManager)
{
- private readonly IActivityManager _activityManager;
+ _activityManager = activityManager;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of interface.
- public ActivityLogController(IActivityManager activityManager)
+ ///
+ /// Gets activity log entries.
+ ///
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. The minimum date. Format = ISO.
+ /// Optional. Filter log entries if it has user id, or not.
+ /// Activity log returned.
+ /// A containing the log entries.
+ [HttpGet("Entries")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetLogEntries(
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] DateTime? minDate,
+ [FromQuery] bool? hasUserId)
+ {
+ return await _activityManager.GetPagedResultAsync(new ActivityLogQuery
{
- _activityManager = activityManager;
- }
-
- ///
- /// Gets activity log entries.
- ///
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. The minimum date. Format = ISO.
- /// Optional. Filter log entries if it has user id, or not.
- /// Activity log returned.
- /// A containing the log entries.
- [HttpGet("Entries")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task>> 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);
- }
+ Skip = startIndex,
+ Limit = limit,
+ MinDate = minDate,
+ HasUserId = hasUserId
+ }).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ApiKeyController.cs b/Jellyfin.Api/Controllers/ApiKeyController.cs
index 024a15349e..991f8cbf20 100644
--- a/Jellyfin.Api/Controllers/ApiKeyController.cs
+++ b/Jellyfin.Api/Controllers/ApiKeyController.cs
@@ -7,70 +7,69 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Authentication controller.
+///
+[Route("Auth")]
+public class ApiKeyController : BaseJellyfinApiController
{
+ private readonly IAuthenticationManager _authenticationManager;
+
///
- /// Authentication controller.
+ /// Initializes a new instance of the class.
///
- [Route("Auth")]
- public class ApiKeyController : BaseJellyfinApiController
+ /// Instance of interface.
+ public ApiKeyController(IAuthenticationManager authenticationManager)
{
- private readonly IAuthenticationManager _authenticationManager;
+ _authenticationManager = authenticationManager;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of interface.
- public ApiKeyController(IAuthenticationManager authenticationManager)
- {
- _authenticationManager = authenticationManager;
- }
+ ///
+ /// Get all keys.
+ ///
+ /// Api keys retrieved.
+ /// A with all keys.
+ [HttpGet("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetKeys()
+ {
+ var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
- ///
- /// Get all keys.
- ///
- /// Api keys retrieved.
- /// A with all keys.
- [HttpGet("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task>> GetKeys()
- {
- var keys = await _authenticationManager.GetApiKeys().ConfigureAwait(false);
+ return new QueryResult(keys);
+ }
- return new QueryResult(keys);
- }
+ ///
+ /// Create a new api key.
+ ///
+ /// Name of the app using the authentication key.
+ /// Api key created.
+ /// A .
+ [HttpPost("Keys")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task CreateKey([FromQuery, Required] string app)
+ {
+ await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
- ///
- /// Create a new api key.
- ///
- /// Name of the app using the authentication key.
- /// Api key created.
- /// A .
- [HttpPost("Keys")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task CreateKey([FromQuery, Required] string app)
- {
- await _authenticationManager.CreateApiKey(app).ConfigureAwait(false);
+ return NoContent();
+ }
- return NoContent();
- }
+ ///
+ /// Remove an api key.
+ ///
+ /// The access token to delete.
+ /// Api key deleted.
+ /// A .
+ [HttpDelete("Keys/{key}")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task RevokeKey([FromRoute, Required] string key)
+ {
+ await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
- ///
- /// Remove an api key.
- ///
- /// The access token to delete.
- /// Api key deleted.
- /// A .
- [HttpDelete("Keys/{key}")]
- [Authorize(Policy = Policies.RequiresElevation)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task RevokeKey([FromRoute, Required] string key)
- {
- await _authenticationManager.DeleteApiKey(key).ConfigureAwait(false);
-
- return NoContent();
- }
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs
index c8ac2ed526..c9d2f67f92 100644
--- a/Jellyfin.Api/Controllers/ArtistsController.cs
+++ b/Jellyfin.Api/Controllers/ArtistsController.cs
@@ -1,7 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
@@ -17,464 +16,466 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// The artists controller.
+///
+[Route("Artists")]
+[Authorize]
+public class ArtistsController : BaseJellyfinApiController
{
+ private readonly ILibraryManager _libraryManager;
+ private readonly IUserManager _userManager;
+ private readonly IDtoService _dtoService;
+
///
- /// The artists controller.
+ /// Initializes a new instance of the class.
///
- [Route("Artists")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ArtistsController : BaseJellyfinApiController
+ /// Instance of the interface.
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public ArtistsController(
+ ILibraryManager libraryManager,
+ IUserManager userManager,
+ IDtoService dtoService)
{
- private readonly ILibraryManager _libraryManager;
- private readonly IUserManager _userManager;
- private readonly IDtoService _dtoService;
+ _libraryManager = libraryManager;
+ _userManager = userManager;
+ _dtoService = dtoService;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- /// Instance of the interface.
- public ArtistsController(
- ILibraryManager libraryManager,
- IUserManager userManager,
- IDtoService dtoService)
+ ///
+ /// Gets all artists from a given item, folder, or the entire library.
+ ///
+ /// Optional filter by minimum community rating.
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Search term.
+ /// Specify this to localize the search to a specific item or folder. Omit to use the root.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
+ /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
+ /// Optional. Specify additional filters to apply.
+ /// Optional filter by items that are marked as favorite, or not.
+ /// Optional filter by MediaType. Allows multiple, comma delimited.
+ /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.
+ /// Optional, include user data.
+ /// Optional, the max number of images to return, per image type.
+ /// Optional. The image types to include in the output.
+ /// Optional. If specified, results will be filtered to include only those containing the specified person.
+ /// Optional. If specified, results will be filtered to include only those containing the specified person ids.
+ /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.
+ /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.
+ /// User id.
+ /// Optional filter by items whose name is sorted equally or greater than a given input string.
+ /// Optional filter by items whose name is sorted equally than a given input string.
+ /// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
+ /// Optional, include image information in output.
+ /// Total record count.
+ /// Artists returned.
+ /// An containing the artists.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult> 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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+
+ if (!userId.Value.Equals(default))
{
- _libraryManager = libraryManager;
- _userManager = userManager;
- _dtoService = dtoService;
+ user = _userManager.GetUserById(userId.Value);
}
- ///
- /// Gets all artists from a given item, folder, or the entire library.
- ///
- /// Optional filter by minimum community rating.
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. Search term.
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// Optional. Specify additional fields of information to return in the output.
- /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
- /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply.
- /// Optional filter by items that are marked as favorite, or not.
- /// Optional filter by MediaType. Allows multiple, comma delimited.
- /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.
- /// Optional, include user data.
- /// Optional, the max number of images to return, per image type.
- /// Optional. The image types to include in the output.
- /// Optional. If specified, results will be filtered to include only those containing the specified person.
- /// Optional. If specified, results will be filtered to include only those containing the specified person ids.
- /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.
- /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.
- /// User id.
- /// Optional filter by items whose name is sorted equally or greater than a given input string.
- /// Optional filter by items whose name is sorted equally than a given input string.
- /// Optional filter by items whose name is equally or lesser than a given input string.
- /// Optional. Specify one or more sort orders, comma delimited.
- /// Sort Order - Ascending,Descending.
- /// Optional, include image information in output.
- /// Total record count.
- /// Artists returned.
- /// An containing the artists.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> 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 query = new InternalItemsQuery(user)
{
- var dtoOptions = new DtoOptions { Fields = fields }
- .AddClientFields(User)
- .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+ 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)
+ };
- User? user = null;
- BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
-
- if (userId.HasValue && !userId.Equals(default))
+ if (parentId.HasValue)
+ {
+ if (parentItem is Folder)
{
- user = _userManager.GetUserById(userId.Value);
+ query.AncestorIds = new[] { parentId.Value };
}
-
- var query = new InternalItemsQuery(user)
+ else
{
- 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 };
- }
+ 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(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
}
- ///
- /// Gets all album artists from a given item, folder, or the entire library.
- ///
- /// Optional filter by minimum community rating.
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. Search term.
- /// Specify this to localize the search to a specific item or folder. Omit to use the root.
- /// Optional. Specify additional fields of information to return in the output.
- /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
- /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
- /// Optional. Specify additional filters to apply.
- /// Optional filter by items that are marked as favorite, or not.
- /// Optional filter by MediaType. Allows multiple, comma delimited.
- /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.
- /// Optional, include user data.
- /// Optional, the max number of images to return, per image type.
- /// Optional. The image types to include in the output.
- /// Optional. If specified, results will be filtered to include only those containing the specified person.
- /// Optional. If specified, results will be filtered to include only those containing the specified person ids.
- /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.
- /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.
- /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.
- /// User id.
- /// Optional filter by items whose name is sorted equally or greater than a given input string.
- /// Optional filter by items whose name is sorted equally than a given input string.
- /// Optional filter by items whose name is equally or lesser than a given input string.
- /// Optional. Specify one or more sort orders, comma delimited.
- /// Sort Order - Ascending,Descending.
- /// Optional, include image information in output.
- /// Total record count.
- /// Album artists returned.
- /// An containing the album artists.
- [HttpGet("AlbumArtists")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> 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)
+ // Studios
+ if (studios.Length != 0)
{
- 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))
+ query.StudioIds = studios.Select(i =>
{
- 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)
+ try
{
- query.AncestorIds = new[] { parentId.Value };
+ return _libraryManager.GetStudio(i);
}
- else
+ catch
{
- query.ItemIds = new[] { parentId.Value };
+ return null;
}
- }
-
- // 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(
- query.StartIndex,
- result.TotalRecordCount,
- dtos.ToArray());
+ }).Where(i => i is not null).Select(i => i!.Id).ToArray();
}
- ///
- /// Gets an artist by name.
- ///
- /// Studio name.
- /// Optional. Filter by user id, and attach user data.
- /// Artist returned.
- /// An containing the artist.
- [HttpGet("{name}")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ foreach (var filter in filters)
{
- var dtoOptions = new DtoOptions().AddClientFields(User);
-
- var item = _libraryManager.GetArtist(name, dtoOptions);
-
- if (userId.HasValue && !userId.Value.Equals(default))
+ switch (filter)
{
- 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(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
+
+ ///
+ /// Gets all album artists from a given item, folder, or the entire library.
+ ///
+ /// Optional filter by minimum community rating.
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Search term.
+ /// Specify this to localize the search to a specific item or folder. Omit to use the root.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Optional. If specified, results will be filtered out based on item type. This allows multiple, comma delimited.
+ /// Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.
+ /// Optional. Specify additional filters to apply.
+ /// Optional filter by items that are marked as favorite, or not.
+ /// Optional filter by MediaType. Allows multiple, comma delimited.
+ /// Optional. If specified, results will be filtered based on genre. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on genre id. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on OfficialRating. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on tag. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on production year. This allows multiple, comma delimited.
+ /// Optional, include user data.
+ /// Optional, the max number of images to return, per image type.
+ /// Optional. The image types to include in the output.
+ /// Optional. If specified, results will be filtered to include only those containing the specified person.
+ /// Optional. If specified, results will be filtered to include only those containing the specified person ids.
+ /// Optional. If specified, along with Person, results will be filtered to include only those containing the specified person and PersonType. Allows multiple, comma-delimited.
+ /// Optional. If specified, results will be filtered based on studio. This allows multiple, pipe delimited.
+ /// Optional. If specified, results will be filtered based on studio id. This allows multiple, pipe delimited.
+ /// User id.
+ /// Optional filter by items whose name is sorted equally or greater than a given input string.
+ /// Optional filter by items whose name is sorted equally than a given input string.
+ /// Optional filter by items whose name is equally or lesser than a given input string.
+ /// Optional. Specify one or more sort orders, comma delimited.
+ /// Sort Order - Ascending,Descending.
+ /// Optional, include image information in output.
+ /// Total record count.
+ /// Album artists returned.
+ /// An containing the album artists.
+ [HttpGet("AlbumArtists")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult> 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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions { Fields = fields }
+ .AddClientFields(User)
+ .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
+
+ User? user = null;
+ BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
+
+ if (!userId.Value.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(
+ query.StartIndex,
+ result.TotalRecordCount,
+ dtos.ToArray());
+ }
+
+ ///
+ /// Gets an artist by name.
+ ///
+ /// Studio name.
+ /// Optional. Filter by user id, and attach user data.
+ /// Artist returned.
+ /// An containing the artist.
+ [HttpGet("{name}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetArtistByName([FromRoute, Required] string name, [FromQuery] Guid? userId)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+
+ var item = _libraryManager.GetArtist(name, dtoOptions);
+
+ if (!userId.Value.Equals(default))
+ {
+ var user = _userManager.GetUserById(userId.Value);
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions, user);
+ }
+
+ return _dtoService.GetBaseItemDto(item, dtoOptions);
}
}
diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs
index 94f7a7b827..968193a6f8 100644
--- a/Jellyfin.Api/Controllers/AudioController.cs
+++ b/Jellyfin.Api/Controllers/AudioController.cs
@@ -10,355 +10,354 @@ using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// The audio controller.
+///
+// 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;
+
///
- /// The audio controller.
+ /// Initializes a new instance of the class.
///
- // TODO: In order to authenticate this in the future, Dlna playback will require updating
- public class AudioController : BaseJellyfinApiController
+ /// Instance of .
+ public AudioController(AudioHelper audioHelper)
{
- private readonly AudioHelper _audioHelper;
+ _audioHelper = audioHelper;
+ }
- private readonly TranscodingJobType _transcodingJobType = TranscodingJobType.Progressive;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of .
- public AudioController(AudioHelper audioHelper)
+ ///
+ /// Gets an audio stream.
+ ///
+ /// The item id.
+ /// The audio container.
+ /// 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.
+ /// The streaming parameters.
+ /// The tag.
+ /// Optional. The dlna device profile id to utilize.
+ /// The play session id.
+ /// The segment container.
+ /// The segment length.
+ /// The minimum number of segments.
+ /// The media version id, if playing an alternate version.
+ /// The device id of the client requesting. Used to stop encoding processes when needed.
+ /// 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.
+ /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.
+ /// Whether or not to allow copying of the video stream url.
+ /// Whether or not to allow copying of the audio stream url.
+ /// Optional. Whether to break on non key frames.
+ /// Optional. Specify a specific audio sample rate, e.g. 44100.
+ /// Optional. The maximum audio bit depth.
+ /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.
+ /// Optional. Specify a specific number of audio channels to encode to, e.g. 2.
+ /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2.
+ /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.
+ /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.
+ /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
+ /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
+ /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false.
+ /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.
+ /// Optional. The fixed horizontal resolution of the encoded video.
+ /// Optional. The fixed vertical resolution of the encoded video.
+ /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.
+ /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.
+ /// Optional. Specify the subtitle delivery method.
+ /// Optional.
+ /// Optional. The maximum video bit depth.
+ /// Optional. Whether to require avc.
+ /// Optional. Whether to deinterlace the video.
+ /// Optional. Whether to require a non anamorphic stream.
+ /// Optional. The maximum number of audio channels to transcode.
+ /// Optional. The limit of how many cpu cores to use.
+ /// The live stream id.
+ /// Optional. Whether to enable the MpegtsM2Ts mode.
+ /// 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.
+ /// Optional. Specify a subtitle codec to encode to.
+ /// Optional. The transcoding reason.
+ /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used.
+ /// Optional. The index of the video stream to use. If omitted the first video stream will be used.
+ /// Optional. The .
+ /// Optional. The streaming options.
+ /// Audio stream returned.
+ /// A containing the audio file.
+ [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
+ [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task 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? 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
+ };
- ///
- /// Gets an audio stream.
- ///
- /// The item id.
- /// The audio container.
- /// 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.
- /// The streaming parameters.
- /// The tag.
- /// Optional. The dlna device profile id to utilize.
- /// The play session id.
- /// The segment container.
- /// The segment length.
- /// The minimum number of segments.
- /// The media version id, if playing an alternate version.
- /// The device id of the client requesting. Used to stop encoding processes when needed.
- /// 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.
- /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.
- /// Whether or not to allow copying of the video stream url.
- /// Whether or not to allow copying of the audio stream url.
- /// Optional. Whether to break on non key frames.
- /// Optional. Specify a specific audio sample rate, e.g. 44100.
- /// Optional. The maximum audio bit depth.
- /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.
- /// Optional. Specify a specific number of audio channels to encode to, e.g. 2.
- /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2.
- /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.
- /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.
- /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
- /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
- /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false.
- /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.
- /// Optional. The fixed horizontal resolution of the encoded video.
- /// Optional. The fixed vertical resolution of the encoded video.
- /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.
- /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.
- /// Optional. Specify the subtitle delivery method.
- /// Optional.
- /// Optional. The maximum video bit depth.
- /// Optional. Whether to require avc.
- /// Optional. Whether to deinterlace the video.
- /// Optional. Whether to require a non anamorphic stream.
- /// Optional. The maximum number of audio channels to transcode.
- /// Optional. The limit of how many cpu cores to use.
- /// The live stream id.
- /// Optional. Whether to enable the MpegtsM2Ts mode.
- /// 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.
- /// Optional. Specify a subtitle codec to encode to.
- /// Optional. The transcoding reason.
- /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used.
- /// Optional. The index of the video stream to use. If omitted the first video stream will be used.
- /// Optional. The .
- /// Optional. The streaming options.
- /// Audio stream returned.
- /// A containing the audio file.
- [HttpGet("{itemId}/stream", Name = "GetAudioStream")]
- [HttpHead("{itemId}/stream", Name = "HeadAudioStream")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- public async Task 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? streamOptions)
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets an audio stream.
+ ///
+ /// The item id.
+ /// The audio container.
+ /// 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.
+ /// The streaming parameters.
+ /// The tag.
+ /// Optional. The dlna device profile id to utilize.
+ /// The play session id.
+ /// The segment container.
+ /// The segment length.
+ /// The minimum number of segments.
+ /// The media version id, if playing an alternate version.
+ /// The device id of the client requesting. Used to stop encoding processes when needed.
+ /// 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.
+ /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.
+ /// Whether or not to allow copying of the video stream url.
+ /// Whether or not to allow copying of the audio stream url.
+ /// Optional. Whether to break on non key frames.
+ /// Optional. Specify a specific audio sample rate, e.g. 44100.
+ /// Optional. The maximum audio bit depth.
+ /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.
+ /// Optional. Specify a specific number of audio channels to encode to, e.g. 2.
+ /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2.
+ /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.
+ /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.
+ /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
+ /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
+ /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false.
+ /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.
+ /// Optional. The fixed horizontal resolution of the encoded video.
+ /// Optional. The fixed vertical resolution of the encoded video.
+ /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.
+ /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.
+ /// Optional. Specify the subtitle delivery method.
+ /// Optional.
+ /// Optional. The maximum video bit depth.
+ /// Optional. Whether to require avc.
+ /// Optional. Whether to deinterlace the video.
+ /// Optional. Whether to require a non anamporphic stream.
+ /// Optional. The maximum number of audio channels to transcode.
+ /// Optional. The limit of how many cpu cores to use.
+ /// The live stream id.
+ /// Optional. Whether to enable the MpegtsM2Ts mode.
+ /// 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.
+ /// Optional. Specify a subtitle codec to encode to.
+ /// Optional. The transcoding reason.
+ /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used.
+ /// Optional. The index of the video stream to use. If omitted the first video stream will be used.
+ /// Optional. The .
+ /// Optional. The streaming options.
+ /// Audio stream returned.
+ /// A containing the audio file.
+ [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
+ [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesAudioFile]
+ public async Task 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? streamOptions)
+ {
+ StreamingRequestDto streamingRequest = new StreamingRequestDto
{
- 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
- };
+ 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);
- }
-
- ///
- /// Gets an audio stream.
- ///
- /// The item id.
- /// The audio container.
- /// 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.
- /// The streaming parameters.
- /// The tag.
- /// Optional. The dlna device profile id to utilize.
- /// The play session id.
- /// The segment container.
- /// The segment length.
- /// The minimum number of segments.
- /// The media version id, if playing an alternate version.
- /// The device id of the client requesting. Used to stop encoding processes when needed.
- /// 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.
- /// Whether or not to allow automatic stream copy if requested values match the original source. Defaults to true.
- /// Whether or not to allow copying of the video stream url.
- /// Whether or not to allow copying of the audio stream url.
- /// Optional. Whether to break on non key frames.
- /// Optional. Specify a specific audio sample rate, e.g. 44100.
- /// Optional. The maximum audio bit depth.
- /// Optional. Specify an audio bitrate to encode to, e.g. 128000. If omitted this will be left to encoder defaults.
- /// Optional. Specify a specific number of audio channels to encode to, e.g. 2.
- /// Optional. Specify a maximum number of audio channels to encode to, e.g. 2.
- /// Optional. Specify a specific an encoder profile (varies by encoder), e.g. main, baseline, high.
- /// Optional. Specify a level for the encoder profile (varies by encoder), e.g. 3, 3.1.
- /// Optional. A specific video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
- /// Optional. A specific maximum video framerate to encode to, e.g. 23.976. Generally this should be omitted unless the device has specific requirements.
- /// Whether or not to copy timestamps when transcoding with an offset. Defaults to false.
- /// Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.
- /// Optional. The fixed horizontal resolution of the encoded video.
- /// Optional. The fixed vertical resolution of the encoded video.
- /// Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.
- /// Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.
- /// Optional. Specify the subtitle delivery method.
- /// Optional.
- /// Optional. The maximum video bit depth.
- /// Optional. Whether to require avc.
- /// Optional. Whether to deinterlace the video.
- /// Optional. Whether to require a non anamporphic stream.
- /// Optional. The maximum number of audio channels to transcode.
- /// Optional. The limit of how many cpu cores to use.
- /// The live stream id.
- /// Optional. Whether to enable the MpegtsM2Ts mode.
- /// 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.
- /// Optional. Specify a subtitle codec to encode to.
- /// Optional. The transcoding reason.
- /// Optional. The index of the audio stream to use. If omitted the first audio stream will be used.
- /// Optional. The index of the video stream to use. If omitted the first video stream will be used.
- /// Optional. The .
- /// Optional. The streaming options.
- /// Audio stream returned.
- /// A containing the audio file.
- [HttpGet("{itemId}/stream.{container}", Name = "GetAudioStreamByContainer")]
- [HttpHead("{itemId}/stream.{container}", Name = "HeadAudioStreamByContainer")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesAudioFile]
- public async Task 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? 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);
- }
+ return await _audioHelper.GetAudioStream(_transcodingJobType, streamingRequest).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/BrandingController.cs b/Jellyfin.Api/Controllers/BrandingController.cs
index d3ea412015..3c2c4b4dbd 100644
--- a/Jellyfin.Api/Controllers/BrandingController.cs
+++ b/Jellyfin.Api/Controllers/BrandingController.cs
@@ -4,54 +4,53 @@ using MediaBrowser.Model.Branding;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Branding controller.
+///
+public class BrandingController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
///
- /// Branding controller.
+ /// Initializes a new instance of the class.
///
- public class BrandingController : BaseJellyfinApiController
+ /// Instance of the interface.
+ public BrandingController(IServerConfigurationManager serverConfigurationManager)
{
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- public BrandingController(IServerConfigurationManager serverConfigurationManager)
- {
- _serverConfigurationManager = serverConfigurationManager;
- }
+ ///
+ /// Gets branding configuration.
+ ///
+ /// Branding configuration returned.
+ /// An containing the branding configuration.
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetBrandingOptions()
+ {
+ return _serverConfigurationManager.GetConfiguration("branding");
+ }
- ///
- /// Gets branding configuration.
- ///
- /// Branding configuration returned.
- /// An containing the branding configuration.
- [HttpGet("Configuration")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult GetBrandingOptions()
- {
- return _serverConfigurationManager.GetConfiguration("branding");
- }
-
- ///
- /// Gets branding css.
- ///
- /// Branding css returned.
- /// No branding css configured.
- ///
- /// An containing the branding css if exist,
- /// or a if the css is not configured.
- ///
- [HttpGet("Css")]
- [HttpGet("Css.css", Name = "GetBrandingCss_2")]
- [Produces("text/css")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public ActionResult GetBrandingCss()
- {
- var options = _serverConfigurationManager.GetConfiguration("branding");
- return options.CustomCss ?? string.Empty;
- }
+ ///
+ /// Gets branding css.
+ ///
+ /// Branding css returned.
+ /// No branding css configured.
+ ///
+ /// An containing the branding css if exist,
+ /// or a if the css is not configured.
+ ///
+ [HttpGet("Css")]
+ [HttpGet("Css.css", Name = "GetBrandingCss_2")]
+ [Produces("text/css")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult GetBrandingCss()
+ {
+ var options = _serverConfigurationManager.GetConfiguration("branding");
+ return options.CustomCss ?? string.Empty;
}
}
diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs
index d5b589a3fa..11c4ac3768 100644
--- a/Jellyfin.Api/Controllers/ChannelsController.cs
+++ b/Jellyfin.Api/Controllers/ChannelsController.cs
@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using System.Threading.Tasks;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Enums;
@@ -18,234 +17,236 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Channels Controller.
+///
+[Authorize]
+public class ChannelsController : BaseJellyfinApiController
{
+ private readonly IChannelManager _channelManager;
+ private readonly IUserManager _userManager;
+
///
- /// Channels Controller.
+ /// Initializes a new instance of the class.
///
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ChannelsController : BaseJellyfinApiController
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public ChannelsController(IChannelManager channelManager, IUserManager userManager)
{
- private readonly IChannelManager _channelManager;
- private readonly IUserManager _userManager;
+ _channelManager = channelManager;
+ _userManager = userManager;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- public ChannelsController(IChannelManager channelManager, IUserManager userManager)
+ ///
+ /// Gets available channels.
+ ///
+ /// User Id to filter by. Use to not filter by user.
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Filter by channels that support getting latest items.
+ /// Optional. Filter by channels that support media deletion.
+ /// Optional. Filter by channels that are favorite.
+ /// Channels returned.
+ /// An containing the channels.
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetChannels(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery] bool? supportsLatestItems,
+ [FromQuery] bool? supportsMediaDeletion,
+ [FromQuery] bool? isFavorite)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ return await _channelManager.GetChannelsAsync(new ChannelQuery
{
- _channelManager = channelManager;
- _userManager = userManager;
- }
+ Limit = limit,
+ StartIndex = startIndex,
+ UserId = userId.Value,
+ SupportsLatestItems = supportsLatestItems,
+ SupportsMediaDeletion = supportsMediaDeletion,
+ IsFavorite = isFavorite
+ }).ConfigureAwait(false);
+ }
- ///
- /// Gets available channels.
- ///
- /// User Id to filter by. Use to not filter by user.
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. Filter by channels that support getting latest items.
- /// Optional. Filter by channels that support media deletion.
- /// Optional. Filter by channels that are favorite.
- /// Channels returned.
- /// An containing the channels.
- [HttpGet]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetChannels(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery] bool? supportsLatestItems,
- [FromQuery] bool? supportsMediaDeletion,
- [FromQuery] bool? isFavorite)
+ ///
+ /// Get all channel features.
+ ///
+ /// All channel features returned.
+ /// An containing the channel features.
+ [HttpGet("Features")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult> GetAllChannelFeatures()
+ {
+ return _channelManager.GetAllChannelFeatures();
+ }
+
+ ///
+ /// Get channel features.
+ ///
+ /// Channel id.
+ /// Channel features returned.
+ /// An containing the channel features.
+ [HttpGet("{channelId}/Features")]
+ public ActionResult GetChannelFeatures([FromRoute, Required] Guid channelId)
+ {
+ return _channelManager.GetChannelFeatures(channelId);
+ }
+
+ ///
+ /// Get channel items.
+ ///
+ /// Channel Id.
+ /// Optional. Folder Id.
+ /// Optional. User Id.
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Sort Order - Ascending,Descending.
+ /// Optional. Specify additional filters to apply.
+ /// 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.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Channel items returned.
+ ///
+ /// A representing the request to get the channel items.
+ /// The task result contains an containing the channel items.
+ ///
+ [HttpGet("{channelId}/Items")]
+ public async Task>> 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)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = 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,
- StartIndex = startIndex,
- UserId = userId ?? Guid.Empty,
- SupportsLatestItems = supportsLatestItems,
- SupportsMediaDeletion = supportsMediaDeletion,
- IsFavorite = isFavorite
- });
- }
-
- ///
- /// Get all channel features.
- ///
- /// All channel features returned.
- /// An containing the channel features.
- [HttpGet("Features")]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public ActionResult> GetAllChannelFeatures()
- {
- return _channelManager.GetAllChannelFeatures();
- }
-
- ///
- /// Get channel features.
- ///
- /// Channel id.
- /// Channel features returned.
- /// An containing the channel features.
- [HttpGet("{channelId}/Features")]
- public ActionResult GetChannelFeatures([FromRoute, Required] Guid channelId)
- {
- return _channelManager.GetChannelFeatures(channelId);
- }
-
- ///
- /// Get channel items.
- ///
- /// Channel Id.
- /// Optional. Folder Id.
- /// Optional. User Id.
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. Sort Order - Ascending,Descending.
- /// Optional. Specify additional filters to apply.
- /// 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.
- /// Optional. Specify additional fields of information to return in the output.
- /// Channel items returned.
- ///
- /// A representing the request to get the channel items.
- /// The task result contains an containing the channel items.
- ///
- [HttpGet("{channelId}/Items")]
- public async Task>> 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;
- }
+ 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);
}
- ///
- /// Gets latest channel items.
- ///
- /// Optional. User Id.
- /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
- /// Optional. The maximum number of records to return.
- /// Optional. Specify additional filters to apply.
- /// Optional. Specify additional fields of information to return in the output.
- /// Optional. Specify one or more channel id's, comma delimited.
- /// Latest channel items returned.
- ///
- /// A representing the request to get the latest channel items.
- /// The task result contains an containing the latest channel items.
- ///
- [HttpGet("Items/Latest")]
- public async Task>> GetLatestChannelItems(
- [FromQuery] Guid? userId,
- [FromQuery] int? startIndex,
- [FromQuery] int? limit,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
+ return await _channelManager.GetChannelItems(query, CancellationToken.None).ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets latest channel items.
+ ///
+ /// Optional. User Id.
+ /// Optional. The record index to start at. All items with a lower index will be dropped from the results.
+ /// Optional. The maximum number of records to return.
+ /// Optional. Specify additional filters to apply.
+ /// Optional. Specify additional fields of information to return in the output.
+ /// Optional. Specify one or more channel id's, comma delimited.
+ /// Latest channel items returned.
+ ///
+ /// A representing the request to get the latest channel items.
+ /// The task result contains an containing the latest channel items.
+ ///
+ [HttpGet("Items/Latest")]
+ public async Task>> GetLatestChannelItems(
+ [FromQuery] Guid? userId,
+ [FromQuery] int? startIndex,
+ [FromQuery] int? limit,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFilter[] filters,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
+ [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
+ {
+ userId = RequestHelpers.GetUserId(User, userId);
+ var user = userId.Value.Equals(default)
+ ? null
+ : _userManager.GetUserById(userId.Value);
+
+ var query = new InternalItemsQuery(user)
{
- var user = userId is null || userId.Value.Equals(default)
- ? null
- : _userManager.GetUserById(userId.Value);
+ Limit = limit,
+ StartIndex = startIndex,
+ ChannelIds = channelIds,
+ DtoOptions = new DtoOptions { Fields = fields }
+ };
- var query = new InternalItemsQuery(user)
+ foreach (var filter in filters)
+ {
+ switch (filter)
{
- Limit = limit,
- StartIndex = startIndex,
- ChannelIds = channelIds,
- 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;
- }
+ 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.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
+
+ return await _channelManager.GetLatestChannelItems(query, CancellationToken.None).ConfigureAwait(false);
}
}
diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs
index ed073a687e..2c5dbacbbe 100644
--- a/Jellyfin.Api/Controllers/ClientLogController.cs
+++ b/Jellyfin.Api/Controllers/ClientLogController.cs
@@ -1,9 +1,7 @@
using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
-using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions;
-using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.ClientLogDtos;
using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Configuration;
@@ -11,71 +9,70 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Client log controller.
+///
+[Authorize]
+public class ClientLogController : BaseJellyfinApiController
{
+ private const int MaxDocumentSize = 1_000_000;
+ private readonly IClientEventLogger _clientEventLogger;
+ private readonly IServerConfigurationManager _serverConfigurationManager;
+
///
- /// Client log controller.
+ /// Initializes a new instance of the class.
///
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ClientLogController : BaseJellyfinApiController
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public ClientLogController(
+ IClientEventLogger clientEventLogger,
+ IServerConfigurationManager serverConfigurationManager)
{
- private const int MaxDocumentSize = 1_000_000;
- private readonly IClientEventLogger _clientEventLogger;
- private readonly IServerConfigurationManager _serverConfigurationManager;
+ _clientEventLogger = clientEventLogger;
+ _serverConfigurationManager = serverConfigurationManager;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- public ClientLogController(
- IClientEventLogger clientEventLogger,
- IServerConfigurationManager serverConfigurationManager)
+ ///
+ /// Upload a document.
+ ///
+ /// Document saved.
+ /// Event logging disabled.
+ /// Upload size too large.
+ /// Create response.
+ [HttpPost("Document")]
+ [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
+ [AcceptsFile(MediaTypeNames.Text.Plain)]
+ [RequestSizeLimit(MaxDocumentSize)]
+ public async Task> LogFile()
+ {
+ if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
{
- _clientEventLogger = clientEventLogger;
- _serverConfigurationManager = serverConfigurationManager;
+ return Forbid();
}
- ///
- /// Upload a document.
- ///
- /// Document saved.
- /// Event logging disabled.
- /// Upload size too large.
- /// Create response.
- [HttpPost("Document")]
- [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)]
- [AcceptsFile(MediaTypeNames.Text.Plain)]
- [RequestSizeLimit(MaxDocumentSize)]
- public async Task> LogFile()
+ if (Request.ContentLength > MaxDocumentSize)
{
- if (!_serverConfigurationManager.Configuration.AllowClientLogUpload)
- {
- 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));
+ // Manually validate to return proper status code.
+ return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes");
}
- private (string ClientName, string ClientVersion) GetRequestInformation()
- {
- var clientName = HttpContext.User.GetClient() ?? "unknown-client";
- var clientVersion = HttpContext.User.GetIsApiKey()
- ? "apikey"
- : HttpContext.User.GetVersion() ?? "unknown-version";
+ var (clientName, clientVersion) = GetRequestInformation();
+ var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body)
+ .ConfigureAwait(false);
+ return Ok(new ClientLogDocumentResponseDto(fileName));
+ }
- 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);
}
}
diff --git a/Jellyfin.Api/Controllers/CollectionController.cs b/Jellyfin.Api/Controllers/CollectionController.cs
index effc9ed7aa..2db04afb80 100644
--- a/Jellyfin.Api/Controllers/CollectionController.cs
+++ b/Jellyfin.Api/Controllers/CollectionController.cs
@@ -11,101 +11,100 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// The collection controller.
+///
+[Route("Collections")]
+[Authorize(Policy = Policies.CollectionManagement)]
+public class CollectionController : BaseJellyfinApiController
{
+ private readonly ICollectionManager _collectionManager;
+ private readonly IDtoService _dtoService;
+
///
- /// The collection controller.
+ /// Initializes a new instance of the class.
///
- [Route("Collections")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class CollectionController : BaseJellyfinApiController
+ /// Instance of interface.
+ /// Instance of interface.
+ public CollectionController(
+ ICollectionManager collectionManager,
+ IDtoService dtoService)
{
- private readonly ICollectionManager _collectionManager;
- private readonly IDtoService _dtoService;
+ _collectionManager = collectionManager;
+ _dtoService = dtoService;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of interface.
- /// Instance of interface.
- public CollectionController(
- ICollectionManager collectionManager,
- IDtoService dtoService)
+ ///
+ /// Creates a new collection.
+ ///
+ /// The name of the collection.
+ /// Item Ids to add to the collection.
+ /// Optional. Create the collection within a specific folder.
+ /// Whether or not to lock the new collection.
+ /// Collection created.
+ /// A with information about the new collection.
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task> 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;
- _dtoService = dtoService;
- }
+ IsLocked = isLocked,
+ Name = name,
+ ParentId = parentId,
+ ItemIdList = ids,
+ UserIds = new[] { userId }
+ }).ConfigureAwait(false);
- ///
- /// Creates a new collection.
- ///
- /// The name of the collection.
- /// Item Ids to add to the collection.
- /// Optional. Create the collection within a specific folder.
- /// Whether or not to lock the new collection.
- /// Collection created.
- /// A with information about the new collection.
- [HttpPost]
- [ProducesResponseType(StatusCodes.Status200OK)]
- public async Task> CreateCollection(
- [FromQuery] string? name,
- [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] string[] ids,
- [FromQuery] Guid? parentId,
- [FromQuery] bool isLocked = false)
+ var dtoOptions = new DtoOptions().AddClientFields(User);
+
+ var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
+
+ return new CollectionCreationResult
{
- var userId = User.GetUserId();
+ Id = dto.Id
+ };
+ }
- var item = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
- {
- IsLocked = isLocked,
- Name = name,
- ParentId = parentId,
- ItemIdList = ids,
- UserIds = new[] { userId }
- }).ConfigureAwait(false);
+ ///
+ /// Adds items to a collection.
+ ///
+ /// The collection id.
+ /// Item ids, comma delimited.
+ /// Items added to collection.
+ /// A indicating success.
+ [HttpPost("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task 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);
-
- var dto = _dtoService.GetBaseItemDto(item, dtoOptions);
-
- return new CollectionCreationResult
- {
- Id = dto.Id
- };
- }
-
- ///
- /// Adds items to a collection.
- ///
- /// The collection id.
- /// Item ids, comma delimited.
- /// Items added to collection.
- /// A indicating success.
- [HttpPost("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task AddToCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- await _collectionManager.AddToCollectionAsync(collectionId, ids).ConfigureAwait(true);
- return NoContent();
- }
-
- ///
- /// Removes items from a collection.
- ///
- /// The collection id.
- /// Item ids, comma delimited.
- /// Items removed from collection.
- /// A indicating success.
- [HttpDelete("{collectionId}/Items")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- public async Task RemoveFromCollection(
- [FromRoute, Required] Guid collectionId,
- [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
- {
- await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
- return NoContent();
- }
+ ///
+ /// Removes items from a collection.
+ ///
+ /// The collection id.
+ /// Item ids, comma delimited.
+ /// Items removed from collection.
+ /// A indicating success.
+ [HttpDelete("{collectionId}/Items")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public async Task RemoveFromCollection(
+ [FromRoute, Required] Guid collectionId,
+ [FromQuery, Required, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] ids)
+ {
+ await _collectionManager.RemoveFromCollectionAsync(collectionId, ids).ConfigureAwait(false);
+ return NoContent();
}
}
diff --git a/Jellyfin.Api/Controllers/ConfigurationController.cs b/Jellyfin.Api/Controllers/ConfigurationController.cs
index a00ac1b0af..9007dfc410 100644
--- a/Jellyfin.Api/Controllers/ConfigurationController.cs
+++ b/Jellyfin.Api/Controllers/ConfigurationController.cs
@@ -13,124 +13,123 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
-namespace Jellyfin.Api.Controllers
+namespace Jellyfin.Api.Controllers;
+
+///
+/// Configuration Controller.
+///
+[Route("System")]
+[Authorize]
+public class ConfigurationController : BaseJellyfinApiController
{
+ private readonly IServerConfigurationManager _configurationManager;
+ private readonly IMediaEncoder _mediaEncoder;
+
+ private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+
///
- /// Configuration Controller.
+ /// Initializes a new instance of the class.
///
- [Route("System")]
- [Authorize(Policy = Policies.DefaultAuthorization)]
- public class ConfigurationController : BaseJellyfinApiController
+ /// Instance of the interface.
+ /// Instance of the interface.
+ public ConfigurationController(
+ IServerConfigurationManager configurationManager,
+ IMediaEncoder mediaEncoder)
{
- private readonly IServerConfigurationManager _configurationManager;
- private readonly IMediaEncoder _mediaEncoder;
+ _configurationManager = configurationManager;
+ _mediaEncoder = mediaEncoder;
+ }
- private readonly JsonSerializerOptions _serializerOptions = JsonDefaults.Options;
+ ///
+ /// Gets application configuration.
+ ///
+ /// Application configuration returned.
+ /// Application configuration.
+ [HttpGet("Configuration")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult GetConfiguration()
+ {
+ return _configurationManager.Configuration;
+ }
- ///
- /// Initializes a new instance of the class.
- ///
- /// Instance of the interface.
- /// Instance of the interface.
- public ConfigurationController(
- IServerConfigurationManager configurationManager,
- IMediaEncoder mediaEncoder)
+ ///
+ /// Updates application configuration.
+ ///
+ /// Configuration.
+ /// Configuration updated.
+ /// Update status.
+ [HttpPost("Configuration")]
+ [Authorize(Policy = Policies.RequiresElevation)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ public ActionResult UpdateConfiguration([FromBody, Required] ServerConfiguration configuration)
+ {
+ _configurationManager.ReplaceConfiguration(configuration);
+ return NoContent();
+ }
+
+ ///
+ /// Gets a named configuration.
+ ///
+ /// Configuration key.
+ /// Configuration returned.
+ /// Configuration.
+ [HttpGet("Configuration/{key}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesFile(MediaTypeNames.Application.Json)]
+ public ActionResult