mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-04-23 21:47:14 -04:00
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
This commit is contained in:
commit
f3c333f4d5
199 changed files with 7727 additions and 5752 deletions
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.1",
|
||||
"version": "8.0.2",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
28
.devcontainer/Dev - Server Ffmpeg/devcontainer.json
Normal file
28
.devcontainer/Dev - Server Ffmpeg/devcontainer.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"name": "Development Jellyfin Server - FFmpeg",
|
||||
"image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy",
|
||||
// restores nuget packages, installs the dotnet workloads and installs the dev https certificate
|
||||
"postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust; sudo bash \"./.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh\"",
|
||||
// reads the extensions list and installs them
|
||||
"postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/dotnet:2": {
|
||||
"version": "none",
|
||||
"dotnetRuntimeVersions": "8.0",
|
||||
"aspNetCoreRuntimeVersions": "8.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/apt-packages:1": {
|
||||
"preserve_apt_list": false,
|
||||
"packages": ["libfontconfig1"]
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {
|
||||
"dockerDashComposeVersion": "v2"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}
|
||||
},
|
||||
"hostRequirements": {
|
||||
"memory": "8gb",
|
||||
"cpus": 4
|
||||
}
|
||||
}
|
32
.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
Normal file
32
.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
Normal file
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
|
||||
## configure the following for a manuall install of a specific version from the repo
|
||||
|
||||
# wget https://repo.jellyfin.org/releases/server/ubuntu/versions/jellyfin-ffmpeg/6.0.1-1/jellyfin-ffmpeg6_6.0.1-1-jammy_amd64.deb -O ffmpeg.deb
|
||||
|
||||
# sudo apt update
|
||||
# sudo apt install -f ./ffmpeg.deb -y
|
||||
# rm ffmpeg.deb
|
||||
|
||||
|
||||
## Add the jellyfin repo
|
||||
sudo apt install curl gnupg -y
|
||||
sudo apt-get install software-properties-common -y
|
||||
sudo add-apt-repository universe -y
|
||||
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
|
||||
export VERSION_OS="$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release )"
|
||||
export VERSION_CODENAME="$( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release )"
|
||||
export DPKG_ARCHITECTURE="$( dpkg --print-architecture )"
|
||||
cat <<EOF | sudo tee /etc/apt/sources.list.d/jellyfin.sources
|
||||
Types: deb
|
||||
URIs: https://repo.jellyfin.org/${VERSION_OS}
|
||||
Suites: ${VERSION_CODENAME}
|
||||
Components: main
|
||||
Architectures: ${DPKG_ARCHITECTURE}
|
||||
Signed-By: /etc/apt/keyrings/jellyfin.gpg
|
||||
EOF
|
||||
|
||||
sudo apt update -y
|
||||
sudo apt install jellyfin-ffmpeg6 -y
|
49
.github/ISSUE_TEMPLATE/issue report.yml
vendored
49
.github/ISSUE_TEMPLATE/issue report.yml
vendored
|
@ -6,7 +6,11 @@ body:
|
|||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
|
||||
Thanks for taking the time to report an issue. Before submitting a report, please do the following:
|
||||
1. Please head to our forum or chat rooms and troubleshoot with volunteers if you haven't already. Links can be found here: https://jellyfin.org/contact/
|
||||
2. Please search the bug tracker for similar issues. If you do find one, please comment there instead of opening a new bug report.
|
||||
3. If you decide to open a new report, please provide as much detail as possible.
|
||||
4. Please **ONLY** report **ONE** issue per report. If you are experiencing multiple issues, please open multiple reports.
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
|
@ -14,14 +18,18 @@ body:
|
|||
description: Also tell us, what did you expect to happen?
|
||||
placeholder: |
|
||||
The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
|
||||
|
||||
This is my issue.
|
||||
|
||||
Steps to Reproduce
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
If you are using an old release of Jellyfin, please also explain why.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: repro-steps
|
||||
attributes:
|
||||
label: Reproduction Steps
|
||||
placeholder: |
|
||||
1. In this environment...
|
||||
2. With this config...
|
||||
3. Run '...'
|
||||
4. See error...
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -30,11 +38,10 @@ body:
|
|||
label: Jellyfin Version
|
||||
description: What version of Jellyfin are you running?
|
||||
options:
|
||||
- 10.8.z
|
||||
- 10.8.9
|
||||
- 10.7.7
|
||||
- 10.6.4
|
||||
- Other
|
||||
- 10.8.13
|
||||
- 10.8.12
|
||||
- 10.8.11 or older (please specify)
|
||||
- Unstable (master branch)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -77,6 +84,18 @@ body:
|
|||
- Networking:
|
||||
- Storage:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
When providing logs, please keep the following things in mind.
|
||||
1. **DO NOT** use external paste services.
|
||||
2. Please provide complete logs.
|
||||
- For server logs, include everything you think is important plus *10 lines before and after*
|
||||
- For ffmpeg logs, please provide the entire file unmodified.
|
||||
3. Please do not run logs through any translation program. Especially beware if your browser translates pages by default.
|
||||
4. Please do not include logs as screenshots, with the only exception being client logs in browsers.
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
|
@ -84,6 +103,8 @@ body:
|
|||
description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
|
||||
placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
|
||||
render: shell
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: ffmpeg-logs
|
||||
attributes:
|
||||
|
|
34
.github/ISSUE_TEMPLATE/media_playback.md
vendored
34
.github/ISSUE_TEMPLATE/media_playback.md
vendored
|
@ -1,34 +0,0 @@
|
|||
---
|
||||
name: Media playback issue
|
||||
about: Create a media playback issue report
|
||||
title: ''
|
||||
labels: mediaplayback
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Media Info of the file**
|
||||
<!-- Use the Media Info tool (set to text format, download here: https://mediaarea.net/en/MediaInfo) or copy the info from the web ui for the file with the playback issue. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log messages from during the playback issue. -->
|
||||
|
||||
**FFmpeg Logs**
|
||||
<!-- Please paste any FFmpeg logs if remuxing or transcoding appears to be part of the issue. -->
|
||||
|
||||
**Stats for Nerds Screenshots**
|
||||
<!-- If available, add screenshots of the stats for nerds screen to help show the issue problem. -->
|
||||
|
||||
**Server System (please complete the following information):**
|
||||
- OS: [e.g. Docker on Linux, Docker on Windows, Debian, Windows]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
- Hardware settings & device: [e.g. NVENC on GTX1060, VAAPI on Intel i7 8700K]
|
||||
- Reverse proxy: [e.g. no, nginx, apache, etc.]
|
||||
- Other hardware notes: [e.g. Media mounted in CIFS/SMB share, Media mounted from Google Drive]
|
||||
|
||||
**Client System (please complete the following information):**
|
||||
- Device: [e.g. Apple iPhone XS, Xbox One S, LG OLED55C8, Samsung Galaxy Note9, Custom HTPC]
|
||||
- OS: [e.g. iOS, Android, Windows, macOS]
|
||||
- Client: [e.g. Web/Browser, webOS, Android, Android TV, Electron]
|
||||
- Browser (if Web client): [e.g. Firefox, Chrome, Safari]
|
||||
- Client and Browser Version: [e.g. 10.3.4 and 68.0]
|
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
|
@ -27,11 +27,11 @@ jobs:
|
|||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/init@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/autobuild@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1
|
||||
uses: github/codeql-action/analyze@47b3d888fe66b639e431abf22ebca059152f1eea # v3.24.5
|
||||
|
|
14
.github/workflows/ci-openapi.yml
vendored
14
.github/workflows/ci-openapi.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
- 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@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: openapi-head
|
||||
retention-days: 14
|
||||
|
@ -59,7 +59,7 @@ jobs:
|
|||
- 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@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
|
||||
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
|
||||
with:
|
||||
name: openapi-base
|
||||
retention-days: 14
|
||||
|
@ -78,12 +78,12 @@ jobs:
|
|||
- openapi-base
|
||||
steps:
|
||||
- name: Download openapi-head
|
||||
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
|
||||
uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3
|
||||
with:
|
||||
name: openapi-head
|
||||
path: openapi-head
|
||||
- name: Download openapi-base
|
||||
uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1
|
||||
uses: actions/download-artifact@87c55149d96e628cc2ef7e6fc2aab372015aec85 # v4.1.3
|
||||
with:
|
||||
name: openapi-base
|
||||
path: openapi-base
|
||||
|
@ -105,14 +105,14 @@ jobs:
|
|||
body="${body//$'\r'/'%0D'}"
|
||||
echo ::set-output name=body::$body
|
||||
- name: Find difference comment
|
||||
uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
|
||||
uses: peter-evans/find-comment@d5fe37641ad8451bdd80312415672ba26c86575e # v3.0.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@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: ${{ steps.read-diff.outputs.body != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
|||
|
||||
</details>
|
||||
- name: Edit difference comment (unchanged)
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
|
2
.github/workflows/ci-tests.yml
vendored
2
.github/workflows/ci-tests.yml
vendored
|
@ -34,7 +34,7 @@ jobs:
|
|||
--verbosity minimal
|
||||
|
||||
- name: Merge code coverage results
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
|
||||
uses: danielpalme/ReportGenerator-GitHub-Action@b067e0c5d288fb4277b9f397b2dc6013f60381f0 # 5.2.2
|
||||
with:
|
||||
reports: "**/coverage.cobertura.xml"
|
||||
targetdir: "merged/"
|
||||
|
|
10
.github/workflows/commands.yml
vendored
10
.github/workflows/commands.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify as seen
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: ${{ github.event.comment != null }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -58,7 +58,7 @@ jobs:
|
|||
|
||||
- name: Notify as running
|
||||
id: comment_running
|
||||
uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
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@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
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@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0
|
||||
if: ${{ github.event.comment != null && failure() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
2
.vscode/extensions.json
vendored
2
.vscode/extensions.json
vendored
|
@ -2,7 +2,7 @@
|
|||
"recommendations": [
|
||||
"ms-dotnettools.csharp",
|
||||
"editorconfig.editorconfig",
|
||||
"GitHub.vscode-github-actions",
|
||||
"github.vscode-github-actions",
|
||||
"ms-dotnettools.vscode-dotnet-runtime",
|
||||
"ms-dotnettools.csdevkit"
|
||||
],
|
||||
|
|
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
|
@ -29,6 +29,18 @@
|
|||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "ghcs .NET Launch (nowebclient, ffmpeg)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
"program": "${workspaceFolder}/Jellyfin.Server/bin/Debug/net8.0/jellyfin.dll",
|
||||
"args": ["--nowebclient", "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"],
|
||||
"cwd": "${workspaceFolder}/Jellyfin.Server",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": ".NET Attach",
|
||||
"type": "coreclr",
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
- [97carmine](https://github.com/97carmine)
|
||||
- [Abbe98](https://github.com/Abbe98)
|
||||
- [agrenott](https://github.com/agrenott)
|
||||
- [alltilla](https://github.com/alltilla)
|
||||
- [AndreCarvalho](https://github.com/AndreCarvalho)
|
||||
- [anthonylavado](https://github.com/anthonylavado)
|
||||
- [Artiume](https://github.com/Artiume)
|
||||
|
@ -77,6 +78,7 @@
|
|||
- [Marenz](https://github.com/Marenz)
|
||||
- [marius-luca-87](https://github.com/marius-luca-87)
|
||||
- [mark-monteiro](https://github.com/mark-monteiro)
|
||||
- [MarkCiliaVincenti](https://github.com/MarkCiliaVincenti)
|
||||
- [Matt07211](https://github.com/Matt07211)
|
||||
- [Maxr1998](https://github.com/Maxr1998)
|
||||
- [mcarlton00](https://github.com/mcarlton00)
|
||||
|
@ -175,7 +177,9 @@
|
|||
- [Chris-Codes-It](https://github.com/Chris-Codes-It)
|
||||
- [Pithaya](https://github.com/Pithaya)
|
||||
- [Çağrı Sakaoğlu](https://github.com/ilovepilav)
|
||||
_ [Barasingha](https://github.com/MaVdbussche)
|
||||
- [Gauvino](https://github.com/Gauvino)
|
||||
- [felix920506](https://github.com/felix920506)
|
||||
|
||||
# Emby Contributors
|
||||
|
||||
|
@ -247,3 +251,4 @@
|
|||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
- [Robert Lützner](https://github.com/rluetzner)
|
||||
|
|
|
@ -4,34 +4,35 @@
|
|||
</PropertyGroup>
|
||||
<!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
|
||||
<ItemGroup Label="Package Dependencies">
|
||||
<PackageVersion Include="AsyncKeyedLock" Version="6.3.4" />
|
||||
<PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
|
||||
<PackageVersion Include="AutoFixture" Version="4.18.1" />
|
||||
<PackageVersion Include="BDInfo" Version="0.7.6.2" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.0" />
|
||||
<PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.2" />
|
||||
<PackageVersion Include="BlurHashSharp" Version="1.3.2" />
|
||||
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.1" />
|
||||
<PackageVersion Include="Diacritics" Version="3.3.27" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.0" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.2" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||
|
@ -40,14 +41,14 @@
|
|||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.2" />
|
||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageVersion Include="MimeTypes" Version="2.4.0" />
|
||||
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
|
@ -71,20 +72,20 @@
|
|||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.10" />
|
||||
<PackageVersion Include="Svg.Skia" Version="1.0.0.13" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
|
||||
<PackageVersion Include="System.Globalization" Version="4.3.0" />
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
|
||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.1" />
|
||||
<PackageVersion Include="System.Text.Json" Version="8.0.2" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" />
|
||||
<PackageVersion Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageVersion Include="TMDbLib" Version="2.1.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.7" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.6.6" />
|
||||
<PackageVersion Include="xunit" Version="2.7.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -173,6 +173,13 @@ namespace Emby.Naming.Common
|
|||
".vtt",
|
||||
};
|
||||
|
||||
LyricFileExtensions = new[]
|
||||
{
|
||||
".lrc",
|
||||
".elrc",
|
||||
".txt"
|
||||
};
|
||||
|
||||
AlbumStackingPrefixes = new[]
|
||||
{
|
||||
"cd",
|
||||
|
@ -791,6 +798,11 @@ namespace Emby.Naming.Common
|
|||
/// </summary>
|
||||
public string[] SubtitleFileExtensions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of lyric file extensions.
|
||||
/// </summary>
|
||||
public string[] LyricFileExtensions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets list of episode regular expressions.
|
||||
/// </summary>
|
||||
|
|
|
@ -45,7 +45,8 @@ namespace Emby.Naming.ExternalFiles
|
|||
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Lyric && _namingOptions.LyricFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -62,7 +62,6 @@ using MediaBrowser.Controller.MediaEncoding;
|
|||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.QuickConnect;
|
||||
using MediaBrowser.Controller.Resolvers;
|
||||
|
@ -393,7 +392,7 @@ namespace Emby.Server.Implementations
|
|||
/// Runs the startup tasks.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task" />.</returns>
|
||||
public async Task RunStartupTasksAsync()
|
||||
public Task RunStartupTasksAsync()
|
||||
{
|
||||
Logger.LogInformation("Running startup tasks");
|
||||
|
||||
|
@ -405,38 +404,10 @@ namespace Emby.Server.Implementations
|
|||
Resolve<IMediaEncoder>().SetFFmpegPath();
|
||||
|
||||
Logger.LogInformation("ServerId: {ServerId}", SystemId);
|
||||
|
||||
var entryPoints = GetExports<IServerEntryPoint>();
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, true)).ConfigureAwait(false);
|
||||
Logger.LogInformation("Executed all pre-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
|
||||
Logger.LogInformation("Core startup complete");
|
||||
CoreStartupHasCompleted = true;
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||
Logger.LogInformation("Executed all post-startup entry points in {Elapsed:g}", stopWatch.Elapsed);
|
||||
stopWatch.Stop();
|
||||
}
|
||||
|
||||
private IEnumerable<Task> StartEntryPoints(IEnumerable<IServerEntryPoint> entryPoints, bool isBeforeStartup)
|
||||
{
|
||||
foreach (var entryPoint in entryPoints)
|
||||
{
|
||||
if (isBeforeStartup != (entryPoint is IRunBeforeStartup))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogDebug("Starting entry point {Type}", entryPoint.GetType());
|
||||
|
||||
yield return entryPoint.RunAsync();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -659,7 +630,7 @@ namespace Emby.Server.Implementations
|
|||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||
Video.LiveTvManager = Resolve<ILiveTvManager>();
|
||||
Video.RecordingsManager = Resolve<IRecordingsManager>();
|
||||
Folder.UserViewManager = Resolve<IUserViewManager>();
|
||||
UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
|
||||
UserView.CollectionManager = Resolve<ICollectionManager>();
|
||||
|
@ -695,8 +666,6 @@ namespace Emby.Server.Implementations
|
|||
GetExports<IMetadataSaver>(),
|
||||
GetExports<IExternalId>());
|
||||
|
||||
Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<IListingsProvider>());
|
||||
|
||||
Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ using MediaBrowser.Controller.Entities;
|
|||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
@ -47,12 +46,12 @@ namespace Emby.Server.Implementations.Dto
|
|||
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||
|
||||
private readonly ILyricManager _lyricManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
|
||||
public DtoService(
|
||||
|
@ -62,10 +61,10 @@ namespace Emby.Server.Implementations.Dto
|
|||
IItemRepository itemRepo,
|
||||
IImageProcessor imageProcessor,
|
||||
IProviderManager providerManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
IApplicationHost appHost,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||
ILyricManager lyricManager,
|
||||
ITrickplayManager trickplayManager)
|
||||
{
|
||||
_logger = logger;
|
||||
|
@ -74,10 +73,10 @@ namespace Emby.Server.Implementations.Dto
|
|||
_itemRepo = itemRepo;
|
||||
_imageProcessor = imageProcessor;
|
||||
_providerManager = providerManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_appHost = appHost;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
_lyricManager = lyricManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
}
|
||||
|
||||
|
@ -149,10 +148,6 @@ namespace Emby.Server.Implementations.Dto
|
|||
{
|
||||
LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
|
||||
}
|
||||
else if (item is Audio)
|
||||
{
|
||||
dto.HasLyrics = _lyricManager.HasLyricFile(item);
|
||||
}
|
||||
|
||||
if (item is IItemByName itemByName
|
||||
&& options.ContainsField(ItemFields.ItemCounts))
|
||||
|
@ -256,8 +251,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
dto.Etag = item.GetEtag(user);
|
||||
}
|
||||
|
||||
var liveTvManager = LivetvManager;
|
||||
var activeRecording = liveTvManager.GetActiveRecordingInfo(item.Path);
|
||||
var activeRecording = _recordingsManager.GetActiveRecordingInfo(item.Path);
|
||||
if (activeRecording is not null)
|
||||
{
|
||||
dto.Type = BaseItemKind.Recording;
|
||||
|
@ -270,7 +264,12 @@ namespace Emby.Server.Implementations.Dto
|
|||
dto.Name = dto.SeriesName;
|
||||
}
|
||||
|
||||
liveTvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
||||
LivetvManager.AddInfoToRecordingDto(item, dto, activeRecording, user);
|
||||
}
|
||||
|
||||
if (item is Audio audio)
|
||||
{
|
||||
dto.HasLyrics = audio.GetMediaStreams().Any(s => s.Type == MediaStreamType.Lyric);
|
||||
}
|
||||
|
||||
return dto;
|
||||
|
|
|
@ -13,19 +13,19 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="IServerEntryPoint"/> that notifies users when libraries are updated.
|
||||
/// A <see cref="IHostedService"/> responsible for notifying users when libraries are updated.
|
||||
/// </summary>
|
||||
public sealed class LibraryChangedNotifier : IServerEntryPoint
|
||||
public sealed class LibraryChangedNotifier : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
|
@ -70,7 +70,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_libraryManager.ItemAdded += OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated += OnLibraryItemUpdated;
|
||||
|
@ -83,6 +83,20 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnProviderRefreshProgress(object? sender, GenericEventArgs<Tuple<BaseItem, double>> e)
|
||||
{
|
||||
var item = e.Argument.Item1;
|
||||
|
@ -137,9 +151,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
}
|
||||
|
||||
private void OnProviderRefreshStarted(object? sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
}
|
||||
=> OnProviderRefreshProgress(sender, new GenericEventArgs<Tuple<BaseItem, double>>(new Tuple<BaseItem, double>(e.Argument, 0)));
|
||||
|
||||
private void OnProviderRefreshCompleted(object? sender, GenericEventArgs<BaseItem> e)
|
||||
{
|
||||
|
@ -342,7 +354,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
return item.SourceType == SourceType.Library;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||
private static IEnumerable<string> GetTopParentIds(List<BaseItem> items, List<Folder> allUserRootChildren)
|
||||
{
|
||||
var list = new List<string>();
|
||||
|
||||
|
@ -363,7 +375,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
return list.Distinct(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
private IEnumerable<T> TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
|
||||
private T[] TranslatePhysicalItemToUserLibrary<T>(T item, User user, bool includeIfNotFound = false)
|
||||
where T : BaseItem
|
||||
{
|
||||
// If the physical root changed, return the user root
|
||||
|
@ -384,18 +396,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
|
|||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_libraryManager.ItemAdded -= OnLibraryItemAdded;
|
||||
_libraryManager.ItemUpdated -= OnLibraryItemUpdated;
|
||||
_libraryManager.ItemRemoved -= OnLibraryItemRemoved;
|
||||
|
||||
_providerManager.RefreshCompleted -= OnProviderRefreshCompleted;
|
||||
_providerManager.RefreshStarted -= OnProviderRefreshStarted;
|
||||
_providerManager.RefreshProgress -= OnProviderRefreshProgress;
|
||||
|
||||
if (_libraryUpdateTimer is not null)
|
||||
{
|
||||
_libraryUpdateTimer.Dispose();
|
||||
_libraryUpdateTimer = null;
|
||||
}
|
||||
_libraryUpdateTimer?.Dispose();
|
||||
_libraryUpdateTimer = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
@ -8,14 +6,17 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
public sealed class UserDataChangeNotifier : IServerEntryPoint
|
||||
/// <summary>
|
||||
/// <see cref="IHostedService"/> responsible for notifying users when associated item data is updated.
|
||||
/// </summary>
|
||||
public sealed class UserDataChangeNotifier : IHostedService, IDisposable
|
||||
{
|
||||
private const int UpdateDuration = 500;
|
||||
|
||||
|
@ -23,25 +24,43 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new Dictionary<Guid, List<BaseItem>>();
|
||||
private readonly Dictionary<Guid, List<BaseItem>> _changedItems = new();
|
||||
private readonly object _syncLock = new();
|
||||
|
||||
private readonly object _syncLock = new object();
|
||||
private Timer? _updateTimer;
|
||||
|
||||
public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, IUserManager userManager)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserDataChangeNotifier"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userDataManager">The <see cref="IUserDataManager"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
public UserDataChangeNotifier(
|
||||
IUserDataManager userDataManager,
|
||||
ISessionManager sessionManager,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_userDataManager = userDataManager;
|
||||
_sessionManager = sessionManager;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
public Task RunAsync()
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_userDataManager.UserDataSaved += OnUserDataManagerUserDataSaved;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnUserDataManagerUserDataSaved(object? sender, UserDataSaveEventArgs e)
|
||||
{
|
||||
if (e.SaveReason == UserDataSaveReason.PlaybackProgress)
|
||||
|
@ -103,55 +122,40 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
}
|
||||
|
||||
await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach ((var key, var value) in changes)
|
||||
foreach (var (userId, changedItems) in changes)
|
||||
{
|
||||
await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
|
||||
await _sessionManager.SendMessageToUserSessions(
|
||||
[userId],
|
||||
SessionMessageType.UserDataChanged,
|
||||
() => GetUserDataChangeInfo(userId, changedItems),
|
||||
default).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task SendNotifications(Guid userId, List<BaseItem> changedItems, CancellationToken cancellationToken)
|
||||
{
|
||||
return _sessionManager.SendMessageToUserSessions(new List<Guid> { userId }, SessionMessageType.UserDataChanged, () => GetUserDataChangeInfo(userId, changedItems), cancellationToken);
|
||||
}
|
||||
|
||||
private UserDataChangeInfo GetUserDataChangeInfo(Guid userId, List<BaseItem> changedItems)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
var dtoList = changedItems
|
||||
.DistinctBy(x => x.Id)
|
||||
.Select(i =>
|
||||
{
|
||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||
dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
return dto;
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
var userIdString = userId.ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
return new UserDataChangeInfo
|
||||
{
|
||||
UserId = userIdString,
|
||||
|
||||
UserDataList = dtoList
|
||||
UserId = userId.ToString("N", CultureInfo.InvariantCulture),
|
||||
UserDataList = changedItems
|
||||
.DistinctBy(x => x.Id)
|
||||
.Select(i =>
|
||||
{
|
||||
var dto = _userDataManager.GetUserDataDto(i, user);
|
||||
dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
return dto;
|
||||
})
|
||||
.ToArray()
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_updateTimer is not null)
|
||||
{
|
||||
_updateTimer.Dispose();
|
||||
_updateTimer = null;
|
||||
}
|
||||
|
||||
_userDataManager.UserDataSaved -= OnUserDataManagerUserDataSaved;
|
||||
_updateTimer?.Dispose();
|
||||
_updateTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
@ -11,11 +9,13 @@ using MediaBrowser.Controller.Configuration;
|
|||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
public class LibraryMonitor : ILibraryMonitor
|
||||
/// <inheritdoc cref="ILibraryMonitor" />
|
||||
public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
|
||||
{
|
||||
private readonly ILogger<LibraryMonitor> _logger;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -25,19 +25,19 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <summary>
|
||||
/// The file system watchers.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The affected paths.
|
||||
/// </summary>
|
||||
private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>();
|
||||
private readonly List<FileRefresher> _activeRefreshers = [];
|
||||
|
||||
/// <summary>
|
||||
/// A dynamic list of paths that should be ignored. Added to during our own file system modifications.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
|
||||
|
@ -46,34 +46,31 @@ namespace Emby.Server.Implementations.IO
|
|||
/// <param name="libraryManager">The library manager.</param>
|
||||
/// <param name="configurationManager">The configuration manager.</param>
|
||||
/// <param name="fileSystem">The filesystem.</param>
|
||||
/// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
|
||||
public LibraryMonitor(
|
||||
ILogger<LibraryMonitor> logger,
|
||||
ILibraryManager libraryManager,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IFileSystem fileSystem)
|
||||
IFileSystem fileSystem,
|
||||
IHostApplicationLifetime appLifetime)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_fileSystem = fileSystem;
|
||||
|
||||
appLifetime.ApplicationStarted.Register(Start);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
|
||||
/// </summary>
|
||||
/// <param name="path">The path.</param>
|
||||
private void TemporarilyIgnore(string path)
|
||||
{
|
||||
_tempIgnoredPaths[path] = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReportFileSystemChangeBeginning(string path)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
||||
TemporarilyIgnore(path);
|
||||
_tempIgnoredPaths[path] = path;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
@ -107,14 +104,10 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
var options = _libraryManager.GetLibraryOptions(item);
|
||||
|
||||
if (options is not null)
|
||||
{
|
||||
return options.EnableRealtimeMonitor;
|
||||
}
|
||||
|
||||
return false;
|
||||
return options is not null && options.EnableRealtimeMonitor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Start()
|
||||
{
|
||||
_libraryManager.ItemAdded += OnLibraryManagerItemAdded;
|
||||
|
@ -306,20 +299,11 @@ namespace Emby.Server.Implementations.IO
|
|||
{
|
||||
if (removeFromList)
|
||||
{
|
||||
RemoveWatcherFromList(watcher);
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the watcher from list.
|
||||
/// </summary>
|
||||
/// <param name="watcher">The watcher.</param>
|
||||
private void RemoveWatcherFromList(FileSystemWatcher watcher)
|
||||
{
|
||||
_fileSystemWatchers.TryRemove(watcher.Path, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the Error event of the watcher control.
|
||||
/// </summary>
|
||||
|
@ -352,6 +336,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReportFileSystemChanged(string path)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
|
@ -479,31 +464,15 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
Stop();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
|
||||
namespace Emby.Server.Implementations.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IServerEntryPoint" /> which is responsible for starting the library monitor.
|
||||
/// </summary>
|
||||
public sealed class LibraryMonitorStartup : IServerEntryPoint
|
||||
{
|
||||
private readonly ILibraryMonitor _monitor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LibraryMonitorStartup"/> class.
|
||||
/// </summary>
|
||||
/// <param name="monitor">The library monitor.</param>
|
||||
public LibraryMonitorStartup(ILibraryMonitor monitor)
|
||||
{
|
||||
_monitor = monitor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task RunAsync()
|
||||
{
|
||||
_monitor.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ using Jellyfin.Data.Entities;
|
|||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
|
@ -1022,7 +1021,7 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
// Start by just validating the children of the root, but go no further
|
||||
await RootFolder.ValidateChildren(
|
||||
new SimpleProgress<double>(),
|
||||
new Progress<double>(),
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||
recursive: false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
@ -1030,7 +1029,7 @@ namespace Emby.Server.Implementations.Library
|
|||
await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await GetUserRootFolder().ValidateChildren(
|
||||
new SimpleProgress<double>(),
|
||||
new Progress<double>(),
|
||||
new MetadataRefreshOptions(new DirectoryService(_fileSystem)),
|
||||
recursive: false,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
@ -1048,18 +1047,14 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
await ValidateTopLibraryFolders(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
innerProgress.RegisterAction(pct => progress.Report(pct * 0.96));
|
||||
var innerProgress = new Progress<double>(pct => progress.Report(pct * 0.96));
|
||||
|
||||
// Validate the entire media library
|
||||
await RootFolder.ValidateChildren(innerProgress, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), recursive: true, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
progress.Report(96);
|
||||
|
||||
innerProgress = new ActionableProgress<double>();
|
||||
|
||||
innerProgress.RegisterAction(pct => progress.Report(96 + (pct * .04)));
|
||||
innerProgress = new Progress<double>(pct => progress.Report(96 + (pct * .04)));
|
||||
|
||||
await RunPostScanTasks(innerProgress, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
|
@ -1081,12 +1076,10 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
foreach (var task in tasks)
|
||||
{
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
// Prevent access to modified closure
|
||||
var currentNumComplete = numComplete;
|
||||
|
||||
innerProgress.RegisterAction(pct =>
|
||||
var innerProgress = new Progress<double>(pct =>
|
||||
{
|
||||
double innerPercent = pct;
|
||||
innerPercent /= 100;
|
||||
|
@ -1239,6 +1232,19 @@ namespace Emby.Server.Implementations.Library
|
|||
return item;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T GetItemById<T>(Guid id)
|
||||
where T : BaseItem
|
||||
{
|
||||
var item = GetItemById(id);
|
||||
if (item is T typedItem)
|
||||
{
|
||||
return typedItem;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
|
||||
{
|
||||
if (query.Recursive && !query.ParentId.IsEmpty())
|
||||
|
@ -2954,7 +2960,7 @@ namespace Emby.Server.Implementations.Library
|
|||
Task.Run(() =>
|
||||
{
|
||||
// No need to start if scanning the library because it will handle it
|
||||
ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None);
|
||||
ValidateMediaLibrary(new Progress<double>(), CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -48,20 +48,23 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
try
|
||||
{
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
|
||||
// _logger.LogDebug("Found cached media info");
|
||||
await using (jsonStream.ConfigureAwait(false))
|
||||
{
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
// _logger.LogDebug("Found cached media info");
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Could not open cached media info");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deserializing mediainfo cache");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await jsonStream.DisposeAsync().ConfigureAwait(false);
|
||||
_logger.LogError(ex, "Error opening cached media info");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ using System.Linq;
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
|
@ -52,7 +53,7 @@ namespace Emby.Server.Implementations.Library
|
|||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly AsyncNonKeyedLocker _liveStreamLocker = new(1);
|
||||
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
|
||||
private IMediaSourceProvider[] _providers;
|
||||
|
@ -468,12 +469,10 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
public async Task<Tuple<LiveStreamResponse, IDirectStreamProvider>> OpenLiveStreamInternal(LiveStreamRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
await _liveStreamSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
MediaSourceInfo mediaSource;
|
||||
ILiveStream liveStream;
|
||||
|
||||
try
|
||||
using (await _liveStreamLocker.LockAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var (provider, keyId) = GetProvider(request.OpenToken);
|
||||
|
||||
|
@ -493,10 +492,6 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
_openStreams[mediaSource.LiveStreamId] = liveStream;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -837,9 +832,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(id);
|
||||
|
||||
await _liveStreamSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _liveStreamLocker.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (_openStreams.TryGetValue(id, out ILiveStream liveStream))
|
||||
{
|
||||
|
@ -858,10 +851,6 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_liveStreamSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private (IMediaSourceProvider MediaSourceProvider, string KeyId) GetProvider(string key)
|
||||
|
@ -898,7 +887,7 @@ namespace Emby.Server.Implementations.Library
|
|||
CloseLiveStream(key).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
_liveStreamSemaphore.Dispose();
|
||||
_liveStreamLocker.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"VersionNumber": "Versioon {0}",
|
||||
"ValueSpecialEpisodeName": "Eriepisood - {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
|
||||
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
|
||||
"UserStartedPlayingItemWithValues": "{0} taasesitab {1} seadmes {2}",
|
||||
"UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
|
||||
"UserLockedOutWithName": "Kasutaja {0} lukustati",
|
||||
"UserDeletedWithName": "Kasutaja {0} kustutati",
|
||||
|
@ -52,7 +52,7 @@
|
|||
"PluginUninstalledWithName": "{0} eemaldati",
|
||||
"PluginInstalledWithName": "{0} paigaldati",
|
||||
"Plugin": "Plugin",
|
||||
"Playlists": "Pleilistid",
|
||||
"Playlists": "Esitusloendid",
|
||||
"Photos": "Fotod",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
|
||||
"NotificationOptionVideoPlayback": "Video taasesitus algas",
|
||||
|
@ -123,5 +123,7 @@
|
|||
"External": "Väline",
|
||||
"HearingImpaired": "Kuulmispuudega",
|
||||
"TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
|
||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor"
|
||||
"TaskKeyframeExtractor": "Võtmekaadri ekstraktor",
|
||||
"TaskRefreshTrickplayImages": "Loo eelvaate pildid",
|
||||
"TaskRefreshTrickplayImagesDescription": "Loob eelvaated videotele, kus lubatud."
|
||||
}
|
||||
|
|
3
Emby.Server.Implementations/Localization/Core/ga.json
Normal file
3
Emby.Server.Implementations/Localization/Core/ga.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Albums": "Albaim"
|
||||
}
|
1
Emby.Server.Implementations/Localization/Core/ky.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ky.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -122,5 +122,6 @@
|
|||
"TaskRefreshChapterImagesDescription": "Создава тамбнеил за видеата шти имаат поглавја.",
|
||||
"TaskCleanActivityLogDescription": "Избришува логови на активности постари од определеното време.",
|
||||
"TaskCleanActivityLog": "Избриши Лог на Активности",
|
||||
"External": "Надворешен"
|
||||
"External": "Надворешен",
|
||||
"HearingImpaired": "Оштетен слух"
|
||||
}
|
||||
|
|
|
@ -124,5 +124,7 @@
|
|||
"External": "Luaran",
|
||||
"TaskOptimizeDatabase": "Optimumkan pangkalan data",
|
||||
"TaskKeyframeExtractor": "Ekstrak bingkai kunci",
|
||||
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang."
|
||||
"TaskKeyframeExtractorDescription": "Ekstrak bingkai kunci dari fail video untuk membina HLS playlist yang lebih tepat. Tugas ini mungkin perlukan masa yang panjang.",
|
||||
"TaskRefreshTrickplayImagesDescription": "Jana gambar prebiu Trickplay untuk video dalam perpustakaan.",
|
||||
"TaskRefreshTrickplayImages": "Jana gambar Trickplay"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
{
|
||||
"External": "ବହିଃସ୍ଥ",
|
||||
"Genres": "ଧରଣ"
|
||||
"Genres": "ଧରଣ",
|
||||
"Albums": "ଆଲବମଗୁଡ଼ିକ",
|
||||
"Artists": "କଳାକାରଗୁଡ଼ିକ",
|
||||
"Application": "ଆପ୍ଲିକେସନ",
|
||||
"Books": "ବହିଗୁଡ଼ିକ",
|
||||
"Channels": "ଚ୍ୟାନେଲଗୁଡ଼ିକ",
|
||||
"ChapterNameValue": "ବିଭାଗ {0}",
|
||||
"Collections": "ସଂଗ୍ରହଗୁଡ଼ିକ",
|
||||
"Folders": "ଫୋଲ୍ଡରଗୁଡ଼ିକ"
|
||||
}
|
||||
|
|
|
@ -124,5 +124,7 @@
|
|||
"TaskKeyframeExtractor": "Ekstraktor ključnih sličic",
|
||||
"External": "Zunanji",
|
||||
"TaskKeyframeExtractorDescription": "Iz video datoteke Izvleče ključne sličice, da ustvari bolj natančne sezname predvajanja HLS. Proces lahko traja dolgo časa.",
|
||||
"HearingImpaired": "Oslabljen sluh"
|
||||
"HearingImpaired": "Oslabljen sluh",
|
||||
"TaskRefreshTrickplayImages": "Ustvari Trickplay slike",
|
||||
"TaskRefreshTrickplayImagesDescription": "Ustvari trickplay predoglede za posnetke v omogočenih knjižnicah."
|
||||
}
|
||||
|
|
|
@ -123,5 +123,7 @@
|
|||
"TaskKeyframeExtractor": "Trích Xuất Khung Hình",
|
||||
"TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.",
|
||||
"External": "Bên ngoài",
|
||||
"HearingImpaired": "Khiếm Thính"
|
||||
"HearingImpaired": "Khiếm Thính",
|
||||
"TaskRefreshTrickplayImages": "Tạo Ảnh Xem Trước Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Tạo bản xem trước trịckplay cho video trong thư viện đã bật."
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ using Jellyfin.Data.Events;
|
|||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
@ -371,7 +370,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
|
|||
throw new InvalidOperationException("Cannot execute a Task that is already running");
|
||||
}
|
||||
|
||||
var progress = new SimpleProgress<double>();
|
||||
var progress = new Progress<double>();
|
||||
|
||||
CurrentCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
|
|
|
@ -394,6 +394,7 @@ namespace Emby.Server.Implementations.Session
|
|||
session.PlayState.SubtitleStreamIndex = info.SubtitleStreamIndex;
|
||||
session.PlayState.PlayMethod = info.PlayMethod;
|
||||
session.PlayState.RepeatMode = info.RepeatMode;
|
||||
session.PlayState.PlaybackOrder = info.PlaybackOrder;
|
||||
session.PlaylistItemId = info.PlaylistItemId;
|
||||
|
||||
var nowPlayingQueue = info.NowPlayingQueue;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
@ -25,16 +26,28 @@ namespace Jellyfin.Api.Auth.UserPermissionPolicy
|
|||
/// <inheritdoc />
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, UserPermissionRequirement requirement)
|
||||
{
|
||||
var user = _userManager.GetUserById(context.User.GetUserId());
|
||||
if (user is null)
|
||||
{
|
||||
throw new ResourceNotFoundException();
|
||||
}
|
||||
|
||||
if (user.HasPermission(requirement.RequiredPermission))
|
||||
// Api keys have global permissions, so just succeed the requirement.
|
||||
if (context.User.GetIsApiKey())
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userId = context.User.GetUserId();
|
||||
if (!userId.IsEmpty())
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
@ -48,15 +49,17 @@ public class DisplayPreferencesController : BaseJellyfinApiController
|
|||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult<DisplayPreferencesDto> GetDisplayPreferences(
|
||||
[FromRoute, Required] string displayPreferencesId,
|
||||
[FromQuery, Required] Guid userId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery, Required] string client)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
|
||||
if (!Guid.TryParse(displayPreferencesId, out var itemId))
|
||||
{
|
||||
itemId = displayPreferencesId.GetMD5();
|
||||
}
|
||||
|
||||
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId.Value, itemId, client);
|
||||
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
|
||||
itemPreferences.ItemId = itemId;
|
||||
|
||||
|
@ -113,10 +116,12 @@ public class DisplayPreferencesController : BaseJellyfinApiController
|
|||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "displayPreferencesId", Justification = "Imported from ServiceStack")]
|
||||
public ActionResult UpdateDisplayPreferences(
|
||||
[FromRoute, Required] string displayPreferencesId,
|
||||
[FromQuery, Required] Guid userId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery, Required] string client,
|
||||
[FromBody, Required] DisplayPreferencesDto displayPreferences)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
|
||||
HomeSectionType[] defaults =
|
||||
{
|
||||
HomeSectionType.SmallLibraryTiles,
|
||||
|
@ -134,7 +139,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
|
|||
itemId = displayPreferencesId.GetMD5();
|
||||
}
|
||||
|
||||
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
|
||||
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId.Value, itemId, client);
|
||||
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : null;
|
||||
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
|
||||
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
|
||||
|
@ -204,7 +209,7 @@ public class DisplayPreferencesController : BaseJellyfinApiController
|
|||
itemPrefs.ItemId = itemId;
|
||||
|
||||
// Set all remaining custom preferences.
|
||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId.Value, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
|
||||
_displayPreferencesManager.SaveChanges();
|
||||
|
||||
return NoContent();
|
||||
|
|
|
@ -294,9 +294,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!System.IO.File.Exists(playlistPath))
|
||||
{
|
||||
|
@ -326,10 +324,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
transcodingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
|
@ -1442,95 +1436,80 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
|
||||
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
var released = false;
|
||||
var startTranscoding = false;
|
||||
|
||||
try
|
||||
using (await _transcodeManager.LockAsync(playlistPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var startTranscoding = false;
|
||||
if (System.IO.File.Exists(segmentPath))
|
||||
{
|
||||
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
transcodingLock.Release();
|
||||
released = true;
|
||||
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath);
|
||||
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
|
||||
if (segmentId == -1)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
|
||||
startTranscoding = true;
|
||||
segmentId = 0;
|
||||
}
|
||||
else if (currentTranscodingIndex is null)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if (segmentId < currentTranscodingIndex.Value)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
|
||||
startTranscoding = true;
|
||||
}
|
||||
|
||||
if (startTranscoding)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, segmentExtension, 0);
|
||||
}
|
||||
|
||||
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
|
||||
|
||||
state.WaitForPath = segmentPath;
|
||||
job = await _transcodeManager.StartFfMpeg(
|
||||
state,
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, state, false, segmentId),
|
||||
Request.HttpContext.User.GetUserId(),
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
state.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
// await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentTranscodingIndex = GetCurrentTranscodingIndex(playlistPath, segmentExtension);
|
||||
var segmentGapRequiringTranscodingChange = 24 / state.SegmentLength;
|
||||
|
||||
if (segmentId == -1)
|
||||
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
if (job?.TranscodingThrottler is not null)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because fmp4 init file is being requested");
|
||||
startTranscoding = true;
|
||||
segmentId = 0;
|
||||
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
else if (currentTranscodingIndex is null)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because currentTranscodingIndex=null");
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if (segmentId < currentTranscodingIndex.Value)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because requestedIndex={0} and currentTranscodingIndex={1}", segmentId, currentTranscodingIndex);
|
||||
startTranscoding = true;
|
||||
}
|
||||
else if (segmentId - currentTranscodingIndex.Value > segmentGapRequiringTranscodingChange)
|
||||
{
|
||||
_logger.LogDebug("Starting transcoding because segmentGap is {0} and max allowed gap is {1}. requestedIndex={2}", segmentId - currentTranscodingIndex.Value, segmentGapRequiringTranscodingChange, segmentId);
|
||||
startTranscoding = true;
|
||||
}
|
||||
|
||||
if (startTranscoding)
|
||||
{
|
||||
// If the playlist doesn't already exist, startup ffmpeg
|
||||
try
|
||||
{
|
||||
await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (currentTranscodingIndex.HasValue)
|
||||
{
|
||||
DeleteLastFile(playlistPath, segmentExtension, 0);
|
||||
}
|
||||
|
||||
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
|
||||
|
||||
state.WaitForPath = segmentPath;
|
||||
job = await _transcodeManager.StartFfMpeg(
|
||||
state,
|
||||
playlistPath,
|
||||
GetCommandLineArguments(playlistPath, state, false, segmentId),
|
||||
Request.HttpContext.User.GetUserId(),
|
||||
TranscodingJobType,
|
||||
cancellationTokenSource).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
state.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
// await WaitForMinimumSegmentCount(playlistPath, 1, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
|
||||
if (job?.TranscodingThrottler is not null)
|
||||
{
|
||||
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!released)
|
||||
{
|
||||
transcodingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Creates an instant playlist based on a given song.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
|
@ -63,10 +63,10 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Songs/{id}/InstantMix")]
|
||||
[HttpGet("Songs/{itemId}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromSong(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -75,7 +75,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
|
@ -90,7 +90,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Creates an instant playlist based on a given album.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
|
@ -100,10 +100,10 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Albums/{id}/InstantMix")]
|
||||
[HttpGet("Albums/{itemId}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromAlbum(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -112,7 +112,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var album = _libraryManager.GetItemById(id);
|
||||
var album = _libraryManager.GetItemById(itemId);
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
|
@ -127,7 +127,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Creates an instant playlist based on a given playlist.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
|
@ -137,10 +137,10 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Playlists/{id}/InstantMix")]
|
||||
[HttpGet("Playlists/{itemId}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromPlaylist(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -149,7 +149,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(id);
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(itemId);
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
|
@ -200,7 +200,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Creates an instant playlist based on a given artist.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
|
@ -210,10 +210,10 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Artists/{id}/InstantMix")]
|
||||
[HttpGet("Artists/{itemId}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromArtists(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -222,7 +222,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
|
@ -237,7 +237,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Creates an instant playlist based on a given item.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="userId">Optional. Filter by user id, and attach user data.</param>
|
||||
/// <param name="limit">Optional. The maximum number of records to return.</param>
|
||||
/// <param name="fields">Optional. Specify additional fields of information to return in the output.</param>
|
||||
|
@ -247,10 +247,10 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
/// <param name="enableImageTypes">Optional. The image types to include in the output.</param>
|
||||
/// <response code="200">Instant playlist returned.</response>
|
||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the playlist items.</returns>
|
||||
[HttpGet("Items/{id}/InstantMix")]
|
||||
[HttpGet("Items/{itemId}/InstantMix")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetInstantMixFromItem(
|
||||
[FromRoute, Required] Guid id,
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -259,7 +259,7 @@ public class InstantMixController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
var item = _libraryManager.GetItemById(id);
|
||||
var item = _libraryManager.GetItemById(itemId);
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
|
|
|
@ -7,7 +7,6 @@ using System.Linq;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Helpers;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
|
@ -17,7 +16,6 @@ using Jellyfin.Data.Enums;
|
|||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -313,7 +311,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||
{
|
||||
try
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -915,6 +913,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||
User.GetUserId())
|
||||
{
|
||||
ShortOverview = string.Format(CultureInfo.InvariantCulture, _localization.GetLocalizedString("AppDeviceValues"), User.GetClient(), User.GetDevice()),
|
||||
ItemId = item.Id.ToString("N", CultureInfo.InvariantCulture)
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
|
|
|
@ -6,11 +6,9 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.ModelBinders;
|
||||
using Jellyfin.Api.Models.LibraryStructureDto;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -180,7 +178,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -224,7 +222,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -293,7 +291,7 @@ public class LibraryStructureController : BaseJellyfinApiController
|
|||
// No need to start if scanning the library because it will handle it
|
||||
if (refreshLibrary)
|
||||
{
|
||||
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
await _libraryManager.ValidateMediaLibrary(new Progress<double>(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -43,7 +43,10 @@ namespace Jellyfin.Api.Controllers;
|
|||
public class LiveTvController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILiveTvManager _liveTvManager;
|
||||
private readonly IGuideManager _guideManager;
|
||||
private readonly ITunerHostManager _tunerHostManager;
|
||||
private readonly IListingsManager _listingsManager;
|
||||
private readonly IRecordingsManager _recordingsManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
|
@ -56,7 +59,10 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param>
|
||||
/// <param name="guideManager">Instance of the <see cref="IGuideManager"/> interface.</param>
|
||||
/// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param>
|
||||
/// <param name="listingsManager">Instance of the <see cref="IListingsManager"/> interface.</param>
|
||||
/// <param name="recordingsManager">Instance of the <see cref="IRecordingsManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
|
@ -66,7 +72,10 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
/// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
|
||||
public LiveTvController(
|
||||
ILiveTvManager liveTvManager,
|
||||
IGuideManager guideManager,
|
||||
ITunerHostManager tunerHostManager,
|
||||
IListingsManager listingsManager,
|
||||
IRecordingsManager recordingsManager,
|
||||
IUserManager userManager,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILibraryManager libraryManager,
|
||||
|
@ -76,7 +85,10 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
ITranscodeManager transcodeManager)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_guideManager = guideManager;
|
||||
_tunerHostManager = tunerHostManager;
|
||||
_listingsManager = listingsManager;
|
||||
_recordingsManager = recordingsManager;
|
||||
_userManager = userManager;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_libraryManager = libraryManager;
|
||||
|
@ -624,7 +636,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
|
||||
{
|
||||
var user = body.UserId.IsEmpty() ? null : _userManager.GetUserById(body.UserId);
|
||||
var user = body.UserId.IsNullOrEmpty() ? null : _userManager.GetUserById(body.UserId.Value);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
|
@ -941,9 +953,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<GuideInfo> GetGuideInfo()
|
||||
{
|
||||
return _liveTvManager.GetGuideInfo();
|
||||
}
|
||||
=> _guideManager.GetGuideInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a tuner host.
|
||||
|
@ -1013,7 +1023,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
listingsProviderInfo.Password = Convert.ToHexString(SHA1.HashData(Encoding.UTF8.GetBytes(pw))).ToLowerInvariant();
|
||||
}
|
||||
|
||||
return await _liveTvManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
|
||||
return await _listingsManager.SaveListingProvider(listingsProviderInfo, validateLogin, validateListings).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1027,7 +1037,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
public ActionResult DeleteListingProvider([FromQuery] string? id)
|
||||
{
|
||||
_liveTvManager.DeleteListingsProvider(id);
|
||||
_listingsManager.DeleteListingsProvider(id);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -1048,9 +1058,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[FromQuery] string? type,
|
||||
[FromQuery] string? location,
|
||||
[FromQuery] string? country)
|
||||
{
|
||||
return await _liveTvManager.GetLineups(type, id, country, location).ConfigureAwait(false);
|
||||
}
|
||||
=> await _listingsManager.GetLineups(type, id, country, location).ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets available countries.
|
||||
|
@ -1081,48 +1089,20 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[HttpGet("ChannelMappingOptions")]
|
||||
[Authorize(Policy = Policies.LiveTvAccess)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ChannelMappingOptionsDto>> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
{
|
||||
var config = _configurationManager.GetConfiguration<LiveTvOptions>("livetv");
|
||||
|
||||
var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var listingsProviderName = _liveTvManager.ListingProviders.First(i => string.Equals(i.Type, listingsProviderInfo.Type, StringComparison.OrdinalIgnoreCase)).Name;
|
||||
|
||||
var tunerChannels = await _liveTvManager.GetChannelsForListingsProvider(providerId, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var providerChannels = await _liveTvManager.GetChannelsFromListingsProviderData(providerId, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var mappings = listingsProviderInfo.ChannelMappings;
|
||||
|
||||
return new ChannelMappingOptionsDto
|
||||
{
|
||||
TunerChannels = tunerChannels.Select(i => _liveTvManager.GetTunerChannelMapping(i, mappings, providerChannels)).ToList(),
|
||||
ProviderChannels = providerChannels.Select(i => new NameIdPair
|
||||
{
|
||||
Name = i.Name,
|
||||
Id = i.Id
|
||||
}).ToList(),
|
||||
Mappings = mappings,
|
||||
ProviderName = listingsProviderName
|
||||
};
|
||||
}
|
||||
public Task<ChannelMappingOptionsDto> GetChannelMappingOptions([FromQuery] string? providerId)
|
||||
=> _listingsManager.GetChannelMappingOptions(providerId);
|
||||
|
||||
/// <summary>
|
||||
/// Set channel mappings.
|
||||
/// </summary>
|
||||
/// <param name="setChannelMappingDto">The set channel mapping dto.</param>
|
||||
/// <param name="dto">The set channel mapping dto.</param>
|
||||
/// <response code="200">Created channel mapping returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the created channel mapping.</returns>
|
||||
[HttpPost("ChannelMappings")]
|
||||
[Authorize(Policy = Policies.LiveTvManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<TunerChannelMapping>> SetChannelMapping([FromBody, Required] SetChannelMappingDto setChannelMappingDto)
|
||||
{
|
||||
return await _liveTvManager.SetChannelMapping(setChannelMappingDto.ProviderId, setChannelMappingDto.TunerChannelId, setChannelMappingDto.ProviderChannelId).ConfigureAwait(false);
|
||||
}
|
||||
public Task<TunerChannelMapping> SetChannelMapping([FromBody, Required] SetChannelMappingDto dto)
|
||||
=> _listingsManager.SetChannelMapping(dto.ProviderId, dto.TunerChannelId, dto.ProviderChannelId);
|
||||
|
||||
/// <summary>
|
||||
/// Get tuner host types.
|
||||
|
@ -1164,8 +1144,7 @@ public class LiveTvController : BaseJellyfinApiController
|
|||
[ProducesVideoFile]
|
||||
public ActionResult GetLiveRecordingFile([FromRoute, Required] string recordingId)
|
||||
{
|
||||
var path = _liveTvManager.GetEmbyTvActiveRecordingPath(recordingId);
|
||||
|
||||
var path = _recordingsManager.GetActiveRecordingPath(recordingId);
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
return NotFound();
|
||||
|
|
267
Jellyfin.Api/Controllers/LyricsController.cs
Normal file
267
Jellyfin.Api/Controllers/LyricsController.cs
Normal file
|
@ -0,0 +1,267 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Api;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
using MediaBrowser.Model.Providers;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Lyrics controller.
|
||||
/// </summary>
|
||||
[Route("")]
|
||||
public class LyricsController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly ILyricManager _lyricManager;
|
||||
private readonly IProviderManager _providerManager;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly IUserManager _userManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LyricsController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
|
||||
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
public LyricsController(
|
||||
ILibraryManager libraryManager,
|
||||
ILyricManager lyricManager,
|
||||
IProviderManager providerManager,
|
||||
IFileSystem fileSystem,
|
||||
IUserManager userManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_lyricManager = lyricManager;
|
||||
_providerManager = providerManager;
|
||||
_fileSystem = fileSystem;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an item's lyrics.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <response code="200">Lyrics returned.</response>
|
||||
/// <response code="404">Something went wrong. No Lyrics will be returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
|
||||
[HttpGet("Audio/{itemId}/Lyrics")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LyricDto>> GetLyrics([FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var isApiKey = User.GetIsApiKey();
|
||||
var userId = User.GetUserId();
|
||||
if (!isApiKey && userId.IsEmpty())
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||
if (audio is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!isApiKey)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Check the item is visible for the user
|
||||
if (!audio.IsVisible(user))
|
||||
{
|
||||
return Unauthorized($"{user.Username} is not permitted to access item {audio.Name}.");
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _lyricManager.GetLyricsAsync(audio, CancellationToken.None).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload an external lyric file.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item the lyric belongs to.</param>
|
||||
/// <param name="fileName">Name of the file being uploaded.</param>
|
||||
/// <response code="200">Lyrics uploaded.</response>
|
||||
/// <response code="400">Error processing upload.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>The uploaded lyric.</returns>
|
||||
[HttpPost("Audio/{itemId}/Lyrics")]
|
||||
[Authorize(Policy = Policies.LyricManagement)]
|
||||
[AcceptsFile(MediaTypeNames.Text.Plain)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LyricDto>> UploadLyrics(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromQuery, Required] string fileName)
|
||||
{
|
||||
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||
if (audio is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (Request.ContentLength.GetValueOrDefault(0) == 0)
|
||||
{
|
||||
return BadRequest("No lyrics uploaded");
|
||||
}
|
||||
|
||||
// Utilize Path.GetExtension as it provides extra path validation.
|
||||
var format = Path.GetExtension(fileName.AsSpan()).RightPart('.').ToString();
|
||||
if (string.IsNullOrEmpty(format))
|
||||
{
|
||||
return BadRequest("Extension is required on filename");
|
||||
}
|
||||
|
||||
var stream = new MemoryStream();
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await Request.Body.CopyToAsync(stream).ConfigureAwait(false);
|
||||
var uploadedLyric = await _lyricManager.UploadLyricAsync(
|
||||
audio,
|
||||
new LyricResponse
|
||||
{
|
||||
Format = format,
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (uploadedLyric is null)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
return Ok(uploadedLyric);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an external lyric file.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <response code="204">Lyric deleted.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("Audio/{itemId}/Lyrics")]
|
||||
[Authorize(Policy = Policies.LyricManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteLyrics(
|
||||
[FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||
if (audio is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await _lyricManager.DeleteLyricsAsync(audio).ConfigureAwait(false);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search remote lyrics.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <response code="200">Lyrics retrieved.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>An array of <see cref="RemoteLyricInfo"/>.</returns>
|
||||
[HttpGet("Audio/{itemId}/RemoteSearch/Lyrics")]
|
||||
[Authorize(Policy = Policies.LyricManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<IReadOnlyList<RemoteLyricInfoDto>>> SearchRemoteLyrics([FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||
if (audio is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var results = await _lyricManager.SearchLyricsAsync(audio, false, CancellationToken.None).ConfigureAwait(false);
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a remote lyric.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="lyricId">The lyric id.</param>
|
||||
/// <response code="200">Lyric downloaded.</response>
|
||||
/// <response code="404">Item not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPost("Audio/{itemId}/RemoteSearch/Lyrics/{lyricId}")]
|
||||
[Authorize(Policy = Policies.LyricManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LyricDto>> DownloadRemoteLyrics(
|
||||
[FromRoute, Required] Guid itemId,
|
||||
[FromRoute, Required] string lyricId)
|
||||
{
|
||||
var audio = _libraryManager.GetItemById<Audio>(itemId);
|
||||
if (audio is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var downloadedLyrics = await _lyricManager.DownloadLyricsAsync(audio, lyricId, CancellationToken.None).ConfigureAwait(false);
|
||||
if (downloadedLyrics is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_providerManager.QueueRefresh(audio.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
return Ok(downloadedLyrics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remote lyrics.
|
||||
/// </summary>
|
||||
/// <param name="lyricId">The remote provider item id.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <response code="404">Lyric not found.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> with the lyric file.</returns>
|
||||
[HttpGet("Providers/Lyrics/{lyricId}")]
|
||||
[Authorize(Policy = Policies.LyricManagement)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<LyricDto>> GetRemoteLyrics([FromRoute, Required] string lyricId)
|
||||
{
|
||||
var result = await _lyricManager.GetRemoteLyricsAsync(lyricId, CancellationToken.None).ConfigureAwait(false);
|
||||
if (result is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
|
@ -64,8 +64,9 @@ public class MediaInfoController : BaseJellyfinApiController
|
|||
/// <returns>A <see cref="Task"/> containing a <see cref="PlaybackInfoResponse"/> with the playback information.</returns>
|
||||
[HttpGet("Items/{itemId}/PlaybackInfo")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery, Required] Guid userId)
|
||||
public async Task<ActionResult<PlaybackInfoResponse>> GetPlaybackInfo([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
return await _mediaInfoHelper.GetPlaybackInfo(
|
||||
itemId,
|
||||
userId)
|
||||
|
|
|
@ -174,7 +174,7 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<QueryResult<BaseItemDto>> GetPlaylistItems(
|
||||
[FromRoute, Required] Guid playlistId,
|
||||
[FromQuery, Required] Guid userId,
|
||||
[FromQuery] Guid? userId,
|
||||
[FromQuery] int? startIndex,
|
||||
[FromQuery] int? limit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields,
|
||||
|
@ -183,15 +183,16 @@ public class PlaylistsController : BaseJellyfinApiController
|
|||
[FromQuery] int? imageTypeLimit,
|
||||
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
|
||||
{
|
||||
userId = RequestHelpers.GetUserId(User, userId);
|
||||
var playlist = (Playlist)_libraryManager.GetItemById(playlistId);
|
||||
if (playlist is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = userId.IsEmpty()
|
||||
var user = userId.IsNullOrEmpty()
|
||||
? null
|
||||
: _userManager.GetUserById(userId);
|
||||
: _userManager.GetUserById(userId.Value);
|
||||
|
||||
var items = playlist.GetManageableItems().ToArray();
|
||||
var count = items.Length;
|
||||
|
|
|
@ -11,7 +11,6 @@ using System.Text;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Models.SubtitleDtos;
|
||||
using MediaBrowser.Common.Api;
|
||||
|
@ -162,17 +161,17 @@ public class SubtitleController : BaseJellyfinApiController
|
|||
/// <summary>
|
||||
/// Gets the remote subtitles.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <param name="subtitleId">The item id.</param>
|
||||
/// <response code="200">File returned.</response>
|
||||
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
|
||||
[HttpGet("Providers/Subtitles/Subtitles/{id}")]
|
||||
[HttpGet("Providers/Subtitles/Subtitles/{subtitleId}")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[Produces(MediaTypeNames.Application.Octet)]
|
||||
[ProducesFile("text/*")]
|
||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string id)
|
||||
public async Task<ActionResult> GetRemoteSubtitles([FromRoute, Required] string subtitleId)
|
||||
{
|
||||
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
|
||||
var result = await _subtitleManager.GetRemoteSubtitles(subtitleId, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
|
||||
}
|
||||
|
@ -407,22 +406,29 @@ public class SubtitleController : BaseJellyfinApiController
|
|||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
new SubtitleResponse
|
||||
{
|
||||
Format = body.Format,
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
IsHearingImpaired = body.IsHearingImpaired,
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
return NoContent();
|
||||
var bytes = Encoding.UTF8.GetBytes(body.Data);
|
||||
var memoryStream = new MemoryStream(bytes, 0, bytes.Length, false, true);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
{
|
||||
using var transform = new FromBase64Transform();
|
||||
var stream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
new SubtitleResponse
|
||||
{
|
||||
Format = body.Format,
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
IsHearingImpaired = body.IsHearingImpaired,
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -188,16 +188,24 @@ public class SystemController : BaseJellyfinApiController
|
|||
/// <param name="name">The name of the log file to get.</param>
|
||||
/// <response code="200">Log file retrieved.</response>
|
||||
/// <response code="403">User does not have permission to get log files.</response>
|
||||
/// <response code="404">Could not find a log file with the name.</response>
|
||||
/// <returns>The log file.</returns>
|
||||
[HttpGet("Logs/Log")]
|
||||
[Authorize(Policy = Policies.RequiresElevation)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesFile(MediaTypeNames.Text.Plain)]
|
||||
public ActionResult GetLogFile([FromQuery, Required] string name)
|
||||
{
|
||||
var file = _fileSystem.GetFiles(_appPaths.LogDirectoryPath)
|
||||
.First(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
var file = _fileSystem
|
||||
.GetFiles(_appPaths.LogDirectoryPath)
|
||||
.FirstOrDefault(i => string.Equals(i.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
return NotFound("Log file not found.");
|
||||
}
|
||||
|
||||
// For older files, assume fully static
|
||||
var fileShare = file.LastWriteTimeUtc < DateTime.UtcNow.AddHours(-1) ? FileShare.Read : FileShare.ReadWrite;
|
||||
|
|
|
@ -18,6 +18,7 @@ using MediaBrowser.Controller.Providers;
|
|||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
@ -539,48 +540,4 @@ public class UserLibraryController : BaseJellyfinApiController
|
|||
|
||||
return _userDataRepository.GetUserDataDto(item, user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an item's lyrics.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="itemId">Item id.</param>
|
||||
/// <response code="200">Lyrics returned.</response>
|
||||
/// <response code="404">Something went wrong. No Lyrics will be returned.</response>
|
||||
/// <returns>An <see cref="OkResult"/> containing the item's lyrics.</returns>
|
||||
[HttpGet("Users/{userId}/Items/{itemId}/Lyrics")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<LyricResponse>> GetLyrics([FromRoute, Required] Guid userId, [FromRoute, Required] Guid itemId)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var item = itemId.IsEmpty()
|
||||
? _libraryManager.GetUserRootFolder()
|
||||
: _libraryManager.GetItemById(itemId);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (item is not UserRootFolder
|
||||
// Check the item is visible for the user
|
||||
&& !item.IsVisible(user))
|
||||
{
|
||||
return Unauthorized($"{user.Username} is not permitted to access item {item.Name}.");
|
||||
}
|
||||
|
||||
var result = await _lyricManager.GetLyrics(item).ConfigureAwait(false);
|
||||
if (result is not null)
|
||||
{
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -458,10 +458,8 @@ public class VideosController : BaseJellyfinApiController
|
|||
return BadRequest($"Input protocol {state.InputProtocol} cannot be streamed statically");
|
||||
}
|
||||
|
||||
var outputPath = state.OutputFilePath;
|
||||
|
||||
// Static stream
|
||||
if (@static.HasValue && @static.Value)
|
||||
if (@static.HasValue && @static.Value && !(state.MediaSource.VideoType == VideoType.BluRay || state.MediaSource.VideoType == VideoType.Dvd))
|
||||
{
|
||||
var contentType = state.GetMimeType("." + state.OutputContainer, false) ?? state.GetMimeType(state.MediaPath);
|
||||
|
||||
|
@ -478,7 +476,7 @@ public class VideosController : BaseJellyfinApiController
|
|||
|
||||
// Need to start ffmpeg (because media can't be returned directly)
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, outputPath, "superfast");
|
||||
var ffmpegCommandLineArguments = _encodingHelper.GetProgressiveVideoFullCommandLine(state, encodingOptions, "superfast");
|
||||
return await FileStreamResponseHelpers.GetTranscodedFile(
|
||||
state,
|
||||
isHeadRequest,
|
||||
|
|
|
@ -211,19 +211,8 @@ public class DynamicHlsHelper
|
|||
var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
|
||||
sdrVideoUrl += "&AllowVideoStreamCopy=false";
|
||||
|
||||
var sdrOutputVideoBitrate = _encodingHelper.GetVideoBitrateParamValue(state.VideoRequest, state.VideoStream, state.OutputVideoCodec);
|
||||
var sdrOutputAudioBitrate = 0;
|
||||
if (EncodingHelper.LosslessAudioCodecs.Contains(state.VideoRequest.AudioCodec, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sdrOutputAudioBitrate = state.AudioStream.BitRate ?? 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
sdrOutputAudioBitrate = _encodingHelper.GetAudioBitrateParam(state.VideoRequest, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
|
@ -325,6 +314,7 @@ public class DynamicHlsHelper
|
|||
if (state.VideoStream is not null && state.VideoStream.VideoRange != VideoRange.Unknown)
|
||||
{
|
||||
var videoRange = state.VideoStream.VideoRange;
|
||||
var videoRangeType = state.VideoStream.VideoRangeType;
|
||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
|
||||
{
|
||||
if (videoRange == VideoRange.SDR)
|
||||
|
@ -334,7 +324,14 @@ public class DynamicHlsHelper
|
|||
|
||||
if (videoRange == VideoRange.HDR)
|
||||
{
|
||||
builder.Append(",VIDEO-RANGE=PQ");
|
||||
if (videoRangeType == VideoRangeType.HLG)
|
||||
{
|
||||
builder.Append(",VIDEO-RANGE=HLG");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(",VIDEO-RANGE=PQ");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -93,9 +93,7 @@ public static class FileStreamResponseHelpers
|
|||
return new OkResult();
|
||||
}
|
||||
|
||||
var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
|
||||
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
try
|
||||
using (await transcodeManager.LockAsync(outputPath, cancellationTokenSource.Token).ConfigureAwait(false))
|
||||
{
|
||||
TranscodingJob? job;
|
||||
if (!File.Exists(outputPath))
|
||||
|
@ -117,9 +115,5 @@ public static class FileStreamResponseHelpers
|
|||
var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
|
||||
return new FileStreamResult(stream, contentType);
|
||||
}
|
||||
finally
|
||||
{
|
||||
transcodingLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -225,7 +225,7 @@ public static class StreamingHelpers
|
|||
|
||||
var ext = string.IsNullOrWhiteSpace(state.OutputContainer)
|
||||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
: ("." + GetContainerFileExtension(state.OutputContainer));
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
|
||||
|
@ -559,4 +559,23 @@ public static class StreamingHelpers
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the container into its file extension.
|
||||
/// </summary>
|
||||
/// <param name="container">The container.</param>
|
||||
private static string? GetContainerFileExtension(string? container)
|
||||
{
|
||||
if (string.Equals(container, "mpegts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "ts";
|
||||
}
|
||||
|
||||
if (string.Equals(container, "matroska", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "mkv";
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ public class GetProgramsDto
|
|||
/// <summary>
|
||||
/// Gets or sets optional. Filter by user id.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum premiere start date.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
|
@ -50,6 +51,18 @@ public class ClientCapabilitiesDto
|
|||
/// </summary>
|
||||
public string? IconUrl { get; set; }
|
||||
|
||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||
// TODO: Remove after 10.9
|
||||
[Obsolete("Unused")]
|
||||
[DefaultValue(false)]
|
||||
public bool? SupportsContentUploading { get; set; }
|
||||
|
||||
// TODO: Remove after 10.9
|
||||
[Obsolete("Unused")]
|
||||
[DefaultValue(false)]
|
||||
public bool? SupportsSync { get; set; }
|
||||
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
|
||||
|
||||
/// <summary>
|
||||
/// Convert the dto to the full <see cref="ClientCapabilities"/> model.
|
||||
/// </summary>
|
||||
|
|
|
@ -506,6 +506,7 @@ namespace Jellyfin.Data.Entities
|
|||
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
|
||||
Permissions.Add(new Permission(PermissionKind.EnableCollectionManagement, false));
|
||||
Permissions.Add(new Permission(PermissionKind.EnableSubtitleManagement, false));
|
||||
Permissions.Add(new Permission(PermissionKind.EnableLyricManagement, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -118,6 +118,11 @@ namespace Jellyfin.Data.Enums
|
|||
/// <summary>
|
||||
/// Whether the user can edit subtitles.
|
||||
/// </summary>
|
||||
EnableSubtitleManagement = 22
|
||||
EnableSubtitleManagement = 22,
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user can edit lyrics.
|
||||
/// </summary>
|
||||
EnableLyricManagement = 23,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Events.Consumers.Library;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an entry in the activity log whenever a lyric download fails.
|
||||
/// </summary>
|
||||
public class LyricDownloadFailureLogger : IEventConsumer<LyricDownloadFailureEventArgs>
|
||||
{
|
||||
private readonly ILocalizationManager _localizationManager;
|
||||
private readonly IActivityManager _activityManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LyricDownloadFailureLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="localizationManager">The localization manager.</param>
|
||||
/// <param name="activityManager">The activity manager.</param>
|
||||
public LyricDownloadFailureLogger(ILocalizationManager localizationManager, IActivityManager activityManager)
|
||||
{
|
||||
_localizationManager = localizationManager;
|
||||
_activityManager = activityManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task OnEvent(LyricDownloadFailureEventArgs eventArgs)
|
||||
{
|
||||
await _activityManager.CreateAsync(new ActivityLog(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
_localizationManager.GetLocalizedString("LyricDownloadFailureFromForItem"),
|
||||
eventArgs.Provider,
|
||||
GetItemName(eventArgs.Item)),
|
||||
"LyricDownloadFailure",
|
||||
Guid.Empty)
|
||||
{
|
||||
ItemId = eventArgs.Item.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||
ShortOverview = eventArgs.Exception.Message
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static string GetItemName(BaseItem item)
|
||||
{
|
||||
var name = item.Name;
|
||||
if (item is Episode episode)
|
||||
{
|
||||
if (episode.IndexNumber.HasValue)
|
||||
{
|
||||
name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"Ep{0} - {1}",
|
||||
episode.IndexNumber.Value,
|
||||
name);
|
||||
}
|
||||
|
||||
if (episode.ParentIndexNumber.HasValue)
|
||||
{
|
||||
name = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"S{0}, {1}",
|
||||
episode.ParentIndexNumber.Value,
|
||||
name);
|
||||
}
|
||||
}
|
||||
|
||||
if (item is IHasSeries hasSeries)
|
||||
{
|
||||
name = hasSeries.SeriesName + " - " + name;
|
||||
}
|
||||
|
||||
if (item is IHasAlbumArtist hasAlbumArtist)
|
||||
{
|
||||
var artists = hasAlbumArtist.AlbumArtists;
|
||||
|
||||
if (artists.Count > 0)
|
||||
{
|
||||
name = artists[0] + " - " + name;
|
||||
}
|
||||
}
|
||||
else if (item is IHasArtist hasArtist)
|
||||
{
|
||||
var artists = hasArtist.Artists;
|
||||
|
||||
if (artists.Count > 0)
|
||||
{
|
||||
name = artists[0] + " - " + name;
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ using MediaBrowser.Controller.Events.Authentication;
|
|||
using MediaBrowser.Controller.Events.Session;
|
||||
using MediaBrowser.Controller.Events.Updates;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Subtitles;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -30,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Events
|
|||
public static void AddEventServices(this IServiceCollection collection)
|
||||
{
|
||||
// Library consumers
|
||||
collection.AddScoped<IEventConsumer<LyricDownloadFailureEventArgs>, LyricDownloadFailureLogger>();
|
||||
collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
|
||||
|
||||
// Security consumers
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" />
|
||||
<PackageReference Include="EFCoreSecondLevelCacheInterceptor" />
|
||||
<PackageReference Include="System.Linq.Async" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" />
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -37,7 +38,7 @@ public class TrickplayManager : ITrickplayManager
|
|||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
||||
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
|
||||
private static readonly AsyncNonKeyedLocker _resourcePool = new(1);
|
||||
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
|
||||
|
||||
/// <summary>
|
||||
|
@ -107,93 +108,92 @@ public class TrickplayManager : ITrickplayManager
|
|||
var imgTempDir = string.Empty;
|
||||
var outputDir = GetTrickplayDirectory(video, width);
|
||||
|
||||
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
|
||||
{
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract images
|
||||
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
|
||||
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
|
||||
|
||||
if (mediaSource is null)
|
||||
{
|
||||
_logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaPath = mediaSource.Path;
|
||||
var mediaStream = mediaSource.VideoStream;
|
||||
var container = mediaSource.Container;
|
||||
|
||||
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
|
||||
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
|
||||
mediaPath,
|
||||
container,
|
||||
mediaSource,
|
||||
mediaStream,
|
||||
width,
|
||||
TimeSpan.FromMilliseconds(options.Interval),
|
||||
options.EnableHwAcceleration,
|
||||
options.ProcessThreads,
|
||||
options.Qscale,
|
||||
options.ProcessPriority,
|
||||
_encodingHelper,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
|
||||
{
|
||||
throw new InvalidOperationException("Null or invalid directory from media encoder.");
|
||||
}
|
||||
|
||||
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
|
||||
.Select(i => i.FullName)
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
|
||||
// Create tiles
|
||||
var trickplayInfo = CreateTiles(images, width, options, outputDir);
|
||||
|
||||
// Save tiles info
|
||||
try
|
||||
{
|
||||
if (trickplayInfo is not null)
|
||||
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
|
||||
{
|
||||
trickplayInfo.ItemId = video.Id;
|
||||
await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
|
||||
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
// Extract images
|
||||
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
|
||||
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
|
||||
|
||||
if (mediaSource is null)
|
||||
{
|
||||
throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
|
||||
_logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var mediaPath = mediaSource.Path;
|
||||
var mediaStream = mediaSource.VideoStream;
|
||||
var container = mediaSource.Container;
|
||||
|
||||
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
|
||||
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
|
||||
mediaPath,
|
||||
container,
|
||||
mediaSource,
|
||||
mediaStream,
|
||||
width,
|
||||
TimeSpan.FromMilliseconds(options.Interval),
|
||||
options.EnableHwAcceleration,
|
||||
options.ProcessThreads,
|
||||
options.Qscale,
|
||||
options.ProcessPriority,
|
||||
_encodingHelper,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
|
||||
{
|
||||
throw new InvalidOperationException("Null or invalid directory from media encoder.");
|
||||
}
|
||||
|
||||
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
|
||||
.Select(i => i.FullName)
|
||||
.OrderBy(i => i)
|
||||
.ToList();
|
||||
|
||||
// Create tiles
|
||||
var trickplayInfo = CreateTiles(images, width, options, outputDir);
|
||||
|
||||
// Save tiles info
|
||||
try
|
||||
{
|
||||
if (trickplayInfo is not null)
|
||||
{
|
||||
trickplayInfo.ItemId = video.Id;
|
||||
await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while saving trickplay tiles info.");
|
||||
|
||||
// Make sure no files stay in metadata folders on failure
|
||||
// if tiles info wasn't saved.
|
||||
Directory.Delete(outputDir, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error while saving trickplay tiles info.");
|
||||
|
||||
// Make sure no files stay in metadata folders on failure
|
||||
// if tiles info wasn't saved.
|
||||
Directory.Delete(outputDir, true);
|
||||
_logger.LogError(ex, "Error creating trickplay images.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating trickplay images.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_resourcePool.Release();
|
||||
|
||||
if (!string.IsNullOrEmpty(imgTempDir))
|
||||
finally
|
||||
{
|
||||
Directory.Delete(imgTempDir, true);
|
||||
if (!string.IsNullOrEmpty(imgTempDir))
|
||||
{
|
||||
Directory.Delete(imgTempDir, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -382,7 +382,7 @@ public class TrickplayManager : ITrickplayManager
|
|||
|
||||
if (trickplayInfo.ThumbnailCount > 0)
|
||||
{
|
||||
const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
|
||||
const string urlFormat = "{0}.jpg?MediaSourceId={1}&api_key={2}";
|
||||
const string decimalFormat = "{0:0.###}";
|
||||
|
||||
var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
|
||||
|
@ -431,7 +431,6 @@ public class TrickplayManager : ITrickplayManager
|
|||
.AppendFormat(
|
||||
CultureInfo.InvariantCulture,
|
||||
urlFormat,
|
||||
width.ToString(CultureInfo.InvariantCulture),
|
||||
i.ToString(CultureInfo.InvariantCulture),
|
||||
itemId.ToString("N"),
|
||||
apiKey)
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Data.Queries;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Users
|
||||
{
|
||||
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
public DeviceAccessEntryPoint(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceManager = deviceManager;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
public Task RunAsync()
|
||||
{
|
||||
_userManager.OnUserUpdated += OnUserUpdated;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
|
||||
{
|
||||
var user = e.Argument;
|
||||
if (!user.HasPermission(PermissionKind.EnableAllDevices))
|
||||
{
|
||||
await UpdateDeviceAccess(user).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDeviceAccess(User user)
|
||||
{
|
||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
|
||||
foreach (var device in existing)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
|
||||
{
|
||||
await _sessionManager.Logout(device).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
76
Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
Normal file
76
Jellyfin.Server.Implementations/Users/DeviceAccessHost.cs
Normal file
|
@ -0,0 +1,76 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Data.Queries;
|
||||
using MediaBrowser.Controller.Devices;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Users;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="IHostedService"/> responsible for managing user device permissions.
|
||||
/// </summary>
|
||||
public sealed class DeviceAccessHost : IHostedService
|
||||
{
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeviceAccessHost"/> class.
|
||||
/// </summary>
|
||||
/// <param name="userManager">The <see cref="IUserManager"/>.</param>
|
||||
/// <param name="deviceManager">The <see cref="IDeviceManager"/>.</param>
|
||||
/// <param name="sessionManager">The <see cref="ISessionManager"/>.</param>
|
||||
public DeviceAccessHost(IUserManager userManager, IDeviceManager deviceManager, ISessionManager sessionManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceManager = deviceManager;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_userManager.OnUserUpdated += OnUserUpdated;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_userManager.OnUserUpdated -= OnUserUpdated;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async void OnUserUpdated(object? sender, GenericEventArgs<User> e)
|
||||
{
|
||||
var user = e.Argument;
|
||||
if (!user.HasPermission(PermissionKind.EnableAllDevices))
|
||||
{
|
||||
await UpdateDeviceAccess(user).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateDeviceAccess(User user)
|
||||
{
|
||||
var existing = (await _deviceManager.GetDevices(new DeviceQuery
|
||||
{
|
||||
UserId = user.Id
|
||||
}).ConfigureAwait(false)).Items;
|
||||
|
||||
foreach (var device in existing)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(device.DeviceId) && !_deviceManager.CanAccessDevice(user, device.DeviceId))
|
||||
{
|
||||
await _sessionManager.Logout(device).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -688,6 +688,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
|
||||
user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
|
||||
user.SetPermission(PermissionKind.EnableSubtitleManagement, policy.EnableSubtitleManagement);
|
||||
user.SetPermission(PermissionKind.EnableLyricManagement, policy.EnableLyricManagement);
|
||||
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
|
||||
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ using Microsoft.OpenApi.Interfaces;
|
|||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using AuthenticationSchemes = Jellyfin.Api.Constants.AuthenticationSchemes;
|
||||
using IPNetwork = System.Net.IPNetwork;
|
||||
|
||||
namespace Jellyfin.Server.Extensions
|
||||
{
|
||||
|
@ -83,6 +82,7 @@ namespace Jellyfin.Server.Extensions
|
|||
options.AddPolicy(Policies.SyncPlayJoinGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.JoinGroup));
|
||||
options.AddPolicy(Policies.SyncPlayIsInGroup, new SyncPlayAccessRequirement(SyncPlayAccessRequirementType.IsInGroup));
|
||||
options.AddPolicy(Policies.SubtitleManagement, new UserPermissionRequirement(PermissionKind.EnableSubtitleManagement));
|
||||
options.AddPolicy(Policies.LyricManagement, new UserPermissionRequirement(PermissionKind.EnableLyricManagement));
|
||||
options.AddPolicy(
|
||||
Policies.RequiresElevation,
|
||||
policy => policy.AddAuthenticationSchemes(AuthenticationSchemes.CustomAuthentication)
|
||||
|
|
|
@ -12,12 +12,17 @@
|
|||
<ServerGarbageCollection>false</ServerGarbageCollection>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<ApplicationIcon>Jellyfin.Server.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharedVersion.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Jellyfin.Server.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources/Configuration/*" />
|
||||
</ItemGroup>
|
||||
|
|
BIN
Jellyfin.Server/Jellyfin.Server.ico
Normal file
BIN
Jellyfin.Server/Jellyfin.Server.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
|
@ -4,8 +4,10 @@ using System.Net.Http;
|
|||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Emby.Server.Implementations.EntryPoints;
|
||||
using Jellyfin.Api.Middleware;
|
||||
using Jellyfin.LiveTv.Extensions;
|
||||
using Jellyfin.LiveTv.Recordings;
|
||||
using Jellyfin.MediaEncoding.Hls.Extensions;
|
||||
using Jellyfin.Networking;
|
||||
using Jellyfin.Networking.HappyEyeballs;
|
||||
|
@ -17,6 +19,7 @@ using Jellyfin.Server.Infrastructure;
|
|||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.XbmcMetadata;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -124,7 +127,13 @@ namespace Jellyfin.Server
|
|||
services.AddHlsPlaylistGenerator();
|
||||
services.AddLiveTvServices();
|
||||
|
||||
services.AddHostedService<RecordingsHost>();
|
||||
services.AddHostedService<AutoDiscoveryHost>();
|
||||
services.AddHostedService<PortForwardingHost>();
|
||||
services.AddHostedService<NfoUserDataSaver>();
|
||||
services.AddHostedService<LibraryChangedNotifier>();
|
||||
services.AddHostedService<UserDataChangeNotifier>();
|
||||
services.AddHostedService<RecordingNotifier>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -89,4 +89,9 @@ public static class Policies
|
|||
/// Policy name for accessing subtitles management.
|
||||
/// </summary>
|
||||
public const string SubtitleManagement = "SubtitleManagement";
|
||||
|
||||
/// <summary>
|
||||
/// Policy name for accessing lyric management.
|
||||
/// </summary>
|
||||
public const string LyricManagement = "LyricManagement";
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CA1003
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Common.Progress
|
||||
{
|
||||
/// <summary>
|
||||
/// Class ActionableProgress.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type for the action parameter.</typeparam>
|
||||
public class ActionableProgress<T> : IProgress<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// The _actions.
|
||||
/// </summary>
|
||||
private Action<T>? _action;
|
||||
|
||||
public event EventHandler<T>? ProgressChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the action.
|
||||
/// </summary>
|
||||
/// <param name="action">The action.</param>
|
||||
public void RegisterAction(Action<T> action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public void Report(T value)
|
||||
{
|
||||
ProgressChanged?.Invoke(this, value);
|
||||
|
||||
_action?.Invoke(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
#pragma warning disable CA1003
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Common.Progress
|
||||
{
|
||||
public class SimpleProgress<T> : IProgress<T>
|
||||
{
|
||||
public event EventHandler<T>? ProgressChanged;
|
||||
|
||||
public void Report(T value)
|
||||
{
|
||||
ProgressChanged?.Invoke(this, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ using System.Text.Json.Serialization;
|
|||
using System.Threading;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
|
@ -53,7 +52,7 @@ namespace MediaBrowser.Controller.Channels
|
|||
query.ChannelIds = new Guid[] { Id };
|
||||
|
||||
// Don't blow up here because it could cause parent screens with other content to fail
|
||||
return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
#pragma warning disable CA1711, CS1591
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public class ImageStream : IDisposable
|
||||
{
|
||||
public ImageStream(Stream stream)
|
||||
{
|
||||
Stream = stream;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the stream.
|
||||
/// </summary>
|
||||
/// <value>The stream.</value>
|
||||
public Stream Stream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the format.
|
||||
/// </summary>
|
||||
/// <value>The format.</value>
|
||||
public ImageFormat Format { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Stream?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
@ -27,6 +28,7 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
{
|
||||
Artists = Array.Empty<string>();
|
||||
AlbumArtists = Array.Empty<string>();
|
||||
LyricFiles = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -65,6 +67,16 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||
[JsonIgnore]
|
||||
public override MediaType MediaType => MediaType.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this audio has lyrics.
|
||||
/// </summary>
|
||||
public bool? HasLyrics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lyric paths.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> LyricFiles { get; set; }
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
|
|
|
@ -13,7 +13,6 @@ using System.Threading.Tasks.Dataflow;
|
|||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Progress;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
@ -429,16 +428,22 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
if (recursive)
|
||||
{
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
var folder = this;
|
||||
innerProgress.RegisterAction(innerPercent =>
|
||||
var innerProgress = new Progress<double>(innerPercent =>
|
||||
{
|
||||
var percent = ProgressHelpers.GetProgress(ProgressHelpers.UpdatedChildItems, ProgressHelpers.ScannedSubfolders, innerPercent);
|
||||
|
||||
progress.Report(percent);
|
||||
|
||||
ProviderManager.OnRefreshProgress(folder, percent);
|
||||
// TODO: this is sometimes being called after the refresh has completed.
|
||||
try
|
||||
{
|
||||
ProviderManager.OnRefreshProgress(folder, percent);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Logger.LogError(e, "Error refreshing folder");
|
||||
}
|
||||
});
|
||||
|
||||
if (validChildrenNeedGeneration)
|
||||
|
@ -461,10 +466,8 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
var container = this as IMetadataContainer;
|
||||
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
var folder = this;
|
||||
innerProgress.RegisterAction(innerPercent =>
|
||||
var innerProgress = new Progress<double>(innerPercent =>
|
||||
{
|
||||
var percent = ProgressHelpers.GetProgress(ProgressHelpers.ScannedSubfolders, ProgressHelpers.RefreshedMetadata, innerPercent);
|
||||
|
||||
|
@ -472,7 +475,15 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
if (recursive)
|
||||
{
|
||||
ProviderManager.OnRefreshProgress(folder, percent);
|
||||
// TODO: this is sometimes being called after the refresh has completed.
|
||||
try
|
||||
{
|
||||
ProviderManager.OnRefreshProgress(folder, percent);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
Logger.LogError(e, "Error refreshing folder");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -572,9 +583,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
var actionBlock = new ActionBlock<int>(
|
||||
async i =>
|
||||
{
|
||||
var innerProgress = new ActionableProgress<double>();
|
||||
|
||||
innerProgress.RegisterAction(innerPercent =>
|
||||
var innerProgress = new Progress<double>(innerPercent =>
|
||||
{
|
||||
// round the percent and only update progress if it changed to prevent excessive UpdateProgress calls
|
||||
var innerPercentRounded = Math.Round(innerPercent);
|
||||
|
@ -922,7 +931,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
query.ChannelIds = new[] { ChannelId };
|
||||
|
||||
// Don't blow up here because it could cause parent screens with other content to fail
|
||||
return ChannelManager.GetChannelItemsInternal(query, new SimpleProgress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
@ -171,7 +171,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
[JsonIgnore]
|
||||
public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
|
||||
|
||||
public static ILiveTvManager LiveTvManager { get; set; }
|
||||
public static IRecordingsManager RecordingsManager { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override SourceType SourceType
|
||||
|
@ -334,7 +334,7 @@ namespace MediaBrowser.Controller.Entities
|
|||
|
||||
protected override bool IsActiveRecording()
|
||||
{
|
||||
return LiveTvManager.GetActiveRecordingInfo(Path) is not null;
|
||||
return RecordingsManager.GetActiveRecordingInfo(Path) is not null;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
|
|
|
@ -168,6 +168,15 @@ namespace MediaBrowser.Controller.Library
|
|||
/// <returns>BaseItem.</returns>
|
||||
BaseItem GetItemById(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item by id, as T.
|
||||
/// </summary>
|
||||
/// <param name="id">The item id.</param>
|
||||
/// <typeparam name="T">The type of item.</typeparam>
|
||||
/// <returns>The item.</returns>
|
||||
T GetItemById<T>(Guid id)
|
||||
where T : BaseItem;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the intros.
|
||||
/// </summary>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Library
|
||||
{
|
||||
public interface ILibraryMonitor : IDisposable
|
||||
/// <summary>
|
||||
/// Service responsible for monitoring library filesystems for changes.
|
||||
/// </summary>
|
||||
public interface ILibraryMonitor
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts this instance.
|
||||
|
|
26
MediaBrowser.Controller/LiveTv/IGuideManager.cs
Normal file
26
MediaBrowser.Controller/LiveTv/IGuideManager.cs
Normal file
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for managing the Live TV guide.
|
||||
/// </summary>
|
||||
public interface IGuideManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the guide information.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="GuideInfo"/>.</returns>
|
||||
GuideInfo GetGuideInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the guide.
|
||||
/// </summary>
|
||||
/// <param name="progress">The <see cref="IProgress{T}"/> to use.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
|
||||
/// <returns>Task representing the refresh operation.</returns>
|
||||
Task RefreshGuide(IProgress<double> progress, CancellationToken cancellationToken);
|
||||
}
|
79
MediaBrowser.Controller/LiveTv/IListingsManager.cs
Normal file
79
MediaBrowser.Controller/LiveTv/IListingsManager.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for managing <see cref="IListingsProvider"/>s and mapping
|
||||
/// their channels to channels provided by <see cref="ITunerHost"/>s.
|
||||
/// </summary>
|
||||
public interface IListingsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the listing provider.
|
||||
/// </summary>
|
||||
/// <param name="info">The listing provider information.</param>
|
||||
/// <param name="validateLogin">A value indicating whether to validate login.</param>
|
||||
/// <param name="validateListings">A value indicating whether to validate listings..</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the listing provider.
|
||||
/// </summary>
|
||||
/// <param name="id">The listing provider's id.</param>
|
||||
void DeleteListingsProvider(string? id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lineups.
|
||||
/// </summary>
|
||||
/// <param name="providerType">Type of the provider.</param>
|
||||
/// <param name="providerId">The provider identifier.</param>
|
||||
/// <param name="country">The country.</param>
|
||||
/// <param name="location">The location.</param>
|
||||
/// <returns>The available lineups.</returns>
|
||||
Task<List<NameIdPair>> GetLineups(string? providerType, string? providerId, string? country, string? location);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the programs for a provided channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to retrieve programs for.</param>
|
||||
/// <param name="startDateUtc">The earliest date to retrieve programs for.</param>
|
||||
/// <param name="endDateUtc">The latest date to retrieve programs for.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
|
||||
/// <returns>The available programs.</returns>
|
||||
Task<IEnumerable<ProgramInfo>> GetProgramsAsync(
|
||||
ChannelInfo channel,
|
||||
DateTime startDateUtc,
|
||||
DateTime endDateUtc,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds metadata from the <see cref="IListingsProvider"/>s to the provided channels.
|
||||
/// </summary>
|
||||
/// <param name="channels">The channels.</param>
|
||||
/// <param name="enableCache">A value indicating whether to use the EPG channel cache.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
|
||||
/// <returns>A task representing the metadata population.</returns>
|
||||
Task AddProviderMetadata(IList<ChannelInfo> channels, bool enableCache, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel mapping options for a provider.
|
||||
/// </summary>
|
||||
/// <param name="providerId">The id of the provider to use.</param>
|
||||
/// <returns>The channel mapping options.</returns>
|
||||
Task<ChannelMappingOptionsDto> GetChannelMappingOptions(string? providerId);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the channel mapping.
|
||||
/// </summary>
|
||||
/// <param name="providerId">The id of the provider for the mapping.</param>
|
||||
/// <param name="tunerChannelNumber">The tuner channel number.</param>
|
||||
/// <param name="providerChannelNumber">The provider channel number.</param>
|
||||
/// <returns>The updated channel mapping.</returns>
|
||||
Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
|
||||
}
|
|
@ -10,7 +10,6 @@ using Jellyfin.Data.Entities;
|
|||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
@ -36,8 +35,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <value>The services.</value>
|
||||
IReadOnlyList<ILiveTvService> Services { get; }
|
||||
|
||||
IReadOnlyList<IListingsProvider> ListingProviders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new timer defaults asynchronous.
|
||||
/// </summary>
|
||||
|
@ -67,13 +64,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task.</returns>
|
||||
Task CancelSeriesTimer(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the parts.
|
||||
/// </summary>
|
||||
/// <param name="services">The services.</param>
|
||||
/// <param name="listingProviders">The listing providers.</param>
|
||||
void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the timer.
|
||||
/// </summary>
|
||||
|
@ -114,16 +104,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task{QueryResult{SeriesTimerInfoDto}}.</returns>
|
||||
Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel stream.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <param name="mediaSourceId">The media source identifier.</param>
|
||||
/// <param name="currentLiveStreams">The current live streams.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{StreamResponseInfo}.</returns>
|
||||
Task<Tuple<MediaSourceInfo, ILiveStream>> GetChannelStream(string id, string mediaSourceId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the program.
|
||||
/// </summary>
|
||||
|
@ -174,12 +154,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task.</returns>
|
||||
Task CreateSeriesTimer(SeriesTimerInfoDto timer, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the guide information.
|
||||
/// </summary>
|
||||
/// <returns>GuideInfo.</returns>
|
||||
GuideInfo GetGuideInfo();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recommended programs.
|
||||
/// </summary>
|
||||
|
@ -235,14 +209,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Internal channels.</returns>
|
||||
QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel media sources.
|
||||
/// </summary>
|
||||
/// <param name="item">Item to search for.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||
/// <returns>Channel media sources wrapped in a task.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetChannelMediaSources(BaseItem item, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the information to program dto.
|
||||
/// </summary>
|
||||
|
@ -252,31 +218,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <returns>Task.</returns>
|
||||
Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the listing provider.
|
||||
/// </summary>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <param name="validateLogin">if set to <c>true</c> [validate login].</param>
|
||||
/// <param name="validateListings">if set to <c>true</c> [validate listings].</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings);
|
||||
|
||||
void DeleteListingsProvider(string id);
|
||||
|
||||
Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber);
|
||||
|
||||
TunerChannelMapping GetTunerChannelMapping(ChannelInfo tunerChannel, NameValuePair[] mappings, List<ChannelInfo> providerChannels);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lineups.
|
||||
/// </summary>
|
||||
/// <param name="providerType">Type of the provider.</param>
|
||||
/// <param name="providerId">The provider identifier.</param>
|
||||
/// <param name="country">The country.</param>
|
||||
/// <param name="location">The location.</param>
|
||||
/// <returns>Task<List<NameIdPair>>.</returns>
|
||||
Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the channel information.
|
||||
/// </summary>
|
||||
|
@ -285,14 +226,6 @@ namespace MediaBrowser.Controller.LiveTv
|
|||
/// <param name="user">The user.</param>
|
||||
void AddChannelInfo(IReadOnlyCollection<(BaseItemDto ItemDto, LiveTvChannel Channel)> items, DtoOptions options, User user);
|
||||
|
||||
Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken);
|
||||
|
||||
Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
|
||||
|
||||
string GetEmbyTvActiveRecordingPath(string id);
|
||||
|
||||
ActiveRecordingInfo GetActiveRecordingInfo(string path);
|
||||
|
||||
void AddInfoToRecordingDto(BaseItem item, BaseItemDto dto, ActiveRecordingInfo activeRecordingInfo, User user = null);
|
||||
|
||||
Task<BaseItem[]> GetRecordingFoldersAsync(User user);
|
||||
|
|
55
MediaBrowser.Controller/LiveTv/IRecordingsManager.cs
Normal file
55
MediaBrowser.Controller/LiveTv/IRecordingsManager.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for managing LiveTV recordings.
|
||||
/// </summary>
|
||||
public interface IRecordingsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the path for the provided timer id.
|
||||
/// </summary>
|
||||
/// <param name="id">The timer id.</param>
|
||||
/// <returns>The recording path, or <c>null</c> if none exists.</returns>
|
||||
string? GetActiveRecordingPath(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the information for an active recording.
|
||||
/// </summary>
|
||||
/// <param name="path">The recording path.</param>
|
||||
/// <returns>The <see cref="ActiveRecordingInfo"/>, or <c>null</c> if none exists.</returns>
|
||||
ActiveRecordingInfo? GetActiveRecordingInfo(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the recording folders.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="VirtualFolderInfo"/> for each recording folder.</returns>
|
||||
IEnumerable<VirtualFolderInfo> GetRecordingFolders();
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the recording folders all exist, and removes unused folders.
|
||||
/// </summary>
|
||||
/// <returns>Task.</returns>
|
||||
Task CreateRecordingFolders();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the recording with the provided timer id, if one is active.
|
||||
/// </summary>
|
||||
/// <param name="timerId">The timer id.</param>
|
||||
/// <param name="timer">The timer.</param>
|
||||
void CancelRecording(string timerId, TimerInfo? timer);
|
||||
|
||||
/// <summary>
|
||||
/// Records a stream.
|
||||
/// </summary>
|
||||
/// <param name="recordingInfo">The recording info.</param>
|
||||
/// <param name="channel">The channel associated with the recording timer.</param>
|
||||
/// <param name="recordingEndDate">The time to stop recording.</param>
|
||||
/// <returns>Task representing the recording process.</returns>
|
||||
Task RecordStream(ActiveRecordingInfo recordingInfo, BaseItem channel, DateTime recordingEndDate);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.LiveTv
|
||||
{
|
||||
public class TunerChannelMapping
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string ProviderChannelName { get; set; }
|
||||
|
||||
public string ProviderChannelId { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,5 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Lyrics;
|
||||
|
||||
|
@ -9,16 +16,93 @@ namespace MediaBrowser.Controller.Lyrics;
|
|||
public interface ILyricManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the lyrics.
|
||||
/// Occurs when a lyric download fails.
|
||||
/// </summary>
|
||||
/// <param name="item">The media item.</param>
|
||||
/// <returns>A task representing found lyrics the passed item.</returns>
|
||||
Task<LyricResponse?> GetLyrics(BaseItem item);
|
||||
event EventHandler<LyricDownloadFailureEventArgs> LyricDownloadFailure;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if requested item has a matching local lyric file.
|
||||
/// Search for lyrics for the specified song.
|
||||
/// </summary>
|
||||
/// <param name="item">The media item.</param>
|
||||
/// <returns>True if item has a matching lyric file; otherwise false.</returns>
|
||||
bool HasLyricFile(BaseItem item);
|
||||
/// <param name="audio">The song.</param>
|
||||
/// <param name="isAutomated">Whether the request is automated.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||
/// <returns>The list of lyrics.</returns>
|
||||
Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(
|
||||
Audio audio,
|
||||
bool isAutomated,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Search for lyrics.
|
||||
/// </summary>
|
||||
/// <param name="request">The search request.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||
/// <returns>The list of lyrics.</returns>
|
||||
Task<IReadOnlyList<RemoteLyricInfoDto>> SearchLyricsAsync(
|
||||
LyricSearchRequest request,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Download the lyrics.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="lyricId">The remote lyric id.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||
/// <returns>The downloaded lyrics.</returns>
|
||||
Task<LyricDto?> DownloadLyricsAsync(
|
||||
Audio audio,
|
||||
string lyricId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Download the lyrics.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio.</param>
|
||||
/// <param name="libraryOptions">The library options to use.</param>
|
||||
/// <param name="lyricId">The remote lyric id.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||
/// <returns>The downloaded lyrics.</returns>
|
||||
Task<LyricDto?> DownloadLyricsAsync(
|
||||
Audio audio,
|
||||
LibraryOptions libraryOptions,
|
||||
string lyricId,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Upload new lyrics.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio file the lyrics belong to.</param>
|
||||
/// <param name="lyricResponse">The lyric response.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task<LyricDto?> UploadLyricAsync(Audio audio, LyricResponse lyricResponse);
|
||||
|
||||
/// <summary>
|
||||
/// Get the remote lyrics.
|
||||
/// </summary>
|
||||
/// <param name="id">The remote lyrics id.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for the operation.</param>
|
||||
/// <returns>The lyric response.</returns>
|
||||
Task<LyricDto?> GetRemoteLyricsAsync(string id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the lyrics.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio file to remove lyrics from.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task DeleteLyricsAsync(Audio audio);
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of lyric providers.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>Lyric providers.</returns>
|
||||
IReadOnlyList<LyricProviderInfo> GetSupportedProviders(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Get the existing lyric for the audio.
|
||||
/// </summary>
|
||||
/// <param name="audio">The audio item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The parsed lyric model.</returns>
|
||||
Task<LyricDto?> GetLyricsAsync(Audio audio, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
using MediaBrowser.Controller.Resolvers;
|
||||
using MediaBrowser.Providers.Lyric;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
|
||||
namespace MediaBrowser.Controller.Lyrics;
|
||||
|
||||
|
@ -24,5 +24,5 @@ public interface ILyricParser
|
|||
/// </summary>
|
||||
/// <param name="lyrics">The raw lyrics content.</param>
|
||||
/// <returns>The parsed lyrics or null if invalid.</returns>
|
||||
LyricResponse? ParseLyrics(LyricFile lyrics);
|
||||
LyricDto? ParseLyrics(LyricFile lyrics);
|
||||
}
|
||||
|
|
34
MediaBrowser.Controller/Lyrics/ILyricProvider.cs
Normal file
34
MediaBrowser.Controller/Lyrics/ILyricProvider.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Lyrics;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Lyrics;
|
||||
|
||||
/// <summary>
|
||||
/// Interface ILyricsProvider.
|
||||
/// </summary>
|
||||
public interface ILyricProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the provider name.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Search for lyrics.
|
||||
/// </summary>
|
||||
/// <param name="request">The search request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The list of remote lyrics.</returns>
|
||||
Task<IEnumerable<RemoteLyricInfo>> SearchAsync(LyricSearchRequest request, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Get the lyrics.
|
||||
/// </summary>
|
||||
/// <param name="id">The remote lyric id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The lyric response.</returns>
|
||||
Task<LyricResponse?> GetLyricsAsync(string id, CancellationToken cancellationToken);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
using System;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Lyrics
|
||||
{
|
||||
/// <summary>
|
||||
/// An event that occurs when subtitle downloading fails.
|
||||
/// </summary>
|
||||
public class LyricDownloadFailureEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the item.
|
||||
/// </summary>
|
||||
public required BaseItem Item { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the provider.
|
||||
/// </summary>
|
||||
public required string Provider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the exception.
|
||||
/// </summary>
|
||||
public required Exception Exception { get; set; }
|
||||
}
|
||||
}
|
|
@ -87,6 +87,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
/// <value>The level.</value>
|
||||
public string Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the codec tag.
|
||||
/// </summary>
|
||||
/// <value>The codec tag.</value>
|
||||
public string CodecTag { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the framerate.
|
||||
/// </summary>
|
||||
|
|
|
@ -30,6 +30,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
private const string VaapiAlias = "va";
|
||||
private const string D3d11vaAlias = "dx11";
|
||||
private const string VideotoolboxAlias = "vt";
|
||||
private const string RkmppAlias = "rk";
|
||||
private const string OpenclAlias = "ocl";
|
||||
private const string CudaAlias = "cu";
|
||||
private const string DrmAlias = "dr";
|
||||
|
@ -161,6 +162,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{ "vaapi", hwEncoder + "_vaapi" },
|
||||
{ "videotoolbox", hwEncoder + "_videotoolbox" },
|
||||
{ "v4l2m2m", hwEncoder + "_v4l2m2m" },
|
||||
{ "rkmpp", hwEncoder + "_rkmpp" },
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(hwType)
|
||||
|
@ -217,6 +219,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
&& _mediaEncoder.SupportsFilter("hwupload_vaapi");
|
||||
}
|
||||
|
||||
private bool IsRkmppFullSupported()
|
||||
{
|
||||
return _mediaEncoder.SupportsHwaccel("rkmpp")
|
||||
&& _mediaEncoder.SupportsFilter("scale_rkrga")
|
||||
&& _mediaEncoder.SupportsFilter("vpp_rkrga")
|
||||
&& _mediaEncoder.SupportsFilter("overlay_rkrga");
|
||||
}
|
||||
|
||||
private bool IsOpenclFullSupported()
|
||||
{
|
||||
return _mediaEncoder.SupportsHwaccel("opencl")
|
||||
|
@ -696,6 +706,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return codec.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private string GetRkmppDeviceArgs(string alias)
|
||||
{
|
||||
alias ??= RkmppAlias;
|
||||
|
||||
// device selection in rk is not supported.
|
||||
return " -init_hw_device rkmpp=" + alias;
|
||||
}
|
||||
|
||||
private string GetVideoToolboxDeviceArgs(string alias)
|
||||
{
|
||||
alias ??= VideotoolboxAlias;
|
||||
|
@ -835,30 +853,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
public string GetGraphicalSubCanvasSize(EncodingJobInfo state)
|
||||
{
|
||||
// DVBSUB and DVDSUB use the fixed canvas size 720x576
|
||||
// DVBSUB uses the fixed canvas size 720x576
|
||||
if (state.SubtitleStream is not null
|
||||
&& state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode
|
||||
&& !state.SubtitleStream.IsTextSubtitleStream
|
||||
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(state.SubtitleStream.Codec, "DVDSUB", StringComparison.OrdinalIgnoreCase))
|
||||
&& !string.Equals(state.SubtitleStream.Codec, "DVBSUB", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var inW = state.VideoStream?.Width;
|
||||
var inH = state.VideoStream?.Height;
|
||||
var reqW = state.BaseRequest.Width;
|
||||
var reqH = state.BaseRequest.Height;
|
||||
var reqMaxW = state.BaseRequest.MaxWidth;
|
||||
var reqMaxH = state.BaseRequest.MaxHeight;
|
||||
var subtitleWidth = state.SubtitleStream?.Width;
|
||||
var subtitleHeight = state.SubtitleStream?.Height;
|
||||
|
||||
// setup a relative small canvas_size for overlay_qsv/vaapi to reduce transfer overhead
|
||||
var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, 1080);
|
||||
|
||||
if (overlayW.HasValue && overlayH.HasValue)
|
||||
if (subtitleWidth.HasValue
|
||||
&& subtitleHeight.HasValue
|
||||
&& subtitleWidth.Value > 0
|
||||
&& subtitleHeight.Value > 0)
|
||||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -canvas_size {0}x{1}",
|
||||
overlayW.Value,
|
||||
overlayH.Value);
|
||||
subtitleWidth.Value,
|
||||
subtitleHeight.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1061,6 +1074,33 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
// no videotoolbox hw filter.
|
||||
args.Append(GetVideoToolboxDeviceArgs(VideotoolboxAlias));
|
||||
}
|
||||
else if (string.Equals(optHwaccelType, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!isLinux || !_mediaEncoder.SupportsHwaccel("rkmpp"))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
|
||||
var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
|
||||
if (!isRkmppDecoder && !isRkmppEncoder)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
args.Append(GetRkmppDeviceArgs(RkmppAlias));
|
||||
|
||||
var filterDevArgs = string.Empty;
|
||||
var doOclTonemap = isHwTonemapAvailable && IsOpenclFullSupported();
|
||||
|
||||
if (doOclTonemap && !isRkmppDecoder)
|
||||
{
|
||||
args.Append(GetOpenclDeviceArgs(0, null, RkmppAlias, OpenclAlias));
|
||||
filterDevArgs = GetFilterHwDeviceArgs(OpenclAlias);
|
||||
}
|
||||
|
||||
args.Append(filterDevArgs);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(vidDecoder))
|
||||
{
|
||||
|
@ -1477,8 +1517,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
if (string.Equals(codec, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "h264_rkmpp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "hevc_rkmpp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "av1_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "av1_amf", StringComparison.OrdinalIgnoreCase)
|
||||
|
@ -1918,20 +1960,22 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
profile = "constrained_baseline";
|
||||
}
|
||||
|
||||
// libx264, h264_qsv and h264_nvenc does not support Constrained Baseline profile, force Baseline in this case.
|
||||
// libx264, h264_{qsv,nvenc,rkmpp} does not support Constrained Baseline profile, force Baseline in this case.
|
||||
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
&& profile.Contains("baseline", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profile = "baseline";
|
||||
}
|
||||
|
||||
// libx264, h264_qsv, h264_nvenc and h264_vaapi does not support Constrained High profile, force High in this case.
|
||||
// libx264, h264_{qsv,nvenc,vaapi,rkmpp} does not support Constrained High profile, force High in this case.
|
||||
if ((string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_qsv", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase))
|
||||
|| string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
&& profile.Contains("high", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
profile = "high";
|
||||
|
@ -2015,6 +2059,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
param += " -level " + level;
|
||||
}
|
||||
}
|
||||
else if (string.Equals(videoEncoder, "h264_rkmpp", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoEncoder, "hevc_rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -level " + level;
|
||||
}
|
||||
else if (!string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
param += " -level " + level;
|
||||
|
@ -2833,6 +2882,48 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return (outputWidth, outputHeight);
|
||||
}
|
||||
|
||||
public static bool IsScaleRatioSupported(
|
||||
int? videoWidth,
|
||||
int? videoHeight,
|
||||
int? requestedWidth,
|
||||
int? requestedHeight,
|
||||
int? requestedMaxWidth,
|
||||
int? requestedMaxHeight,
|
||||
double? maxScaleRatio)
|
||||
{
|
||||
var (outWidth, outHeight) = GetFixedOutputSize(
|
||||
videoWidth,
|
||||
videoHeight,
|
||||
requestedWidth,
|
||||
requestedHeight,
|
||||
requestedMaxWidth,
|
||||
requestedMaxHeight);
|
||||
|
||||
if (!videoWidth.HasValue
|
||||
|| !videoHeight.HasValue
|
||||
|| !outWidth.HasValue
|
||||
|| !outHeight.HasValue
|
||||
|| !maxScaleRatio.HasValue
|
||||
|| (maxScaleRatio.Value < 1.0f))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var minScaleRatio = 1.0f / maxScaleRatio;
|
||||
var scaleRatioW = (double)outWidth / (double)videoWidth;
|
||||
var scaleRatioH = (double)outHeight / (double)videoHeight;
|
||||
|
||||
if (scaleRatioW < minScaleRatio
|
||||
|| scaleRatioW > maxScaleRatio
|
||||
|| scaleRatioH < minScaleRatio
|
||||
|| scaleRatioH > maxScaleRatio)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string GetHwScaleFilter(
|
||||
string hwScaleSuffix,
|
||||
string videoFormat,
|
||||
|
@ -2877,7 +2968,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
public static string GetCustomSwScaleFilter(
|
||||
public static string GetGraphicalSubPreProcessFilters(
|
||||
int? videoWidth,
|
||||
int? videoHeight,
|
||||
int? requestedWidth,
|
||||
|
@ -2897,7 +2988,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
return string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"scale=s={0}x{1}:flags=fast_bilinear",
|
||||
@"scale=-1:{1}:fast_bilinear,scale,crop,pad=max({0}\,iw):max({1}\,ih):(ow-iw)/2:(oh-ih)/2:black@0,crop={0}:{1}",
|
||||
outWidth.Value,
|
||||
outHeight.Value);
|
||||
}
|
||||
|
@ -2913,7 +3004,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
int? requestedHeight,
|
||||
int? requestedMaxWidth,
|
||||
int? requestedMaxHeight,
|
||||
int? framerate)
|
||||
float? framerate)
|
||||
{
|
||||
var reqTicks = state.BaseRequest.StartTimeTicks ?? 0;
|
||||
var startTime = TimeSpan.FromTicks(reqTicks).ToString(@"hh\\\:mm\\\:ss\\\.fff", CultureInfo.InvariantCulture);
|
||||
|
@ -2932,7 +3023,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
"alphasrc=s={0}x{1}:r={2}:start='{3}'",
|
||||
outWidth.Value,
|
||||
outHeight.Value,
|
||||
framerate ?? 10,
|
||||
framerate ?? 25,
|
||||
reqTicks > 0 ? startTime : 0);
|
||||
}
|
||||
|
||||
|
@ -3340,9 +3431,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
}
|
||||
else if (hasGraphicalSubs)
|
||||
{
|
||||
// [0:s]scale=s=1280x720
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
|
||||
|
@ -3504,15 +3594,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale=s=1280x720,format=yuva420p,hwupload
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=yuva420p");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=yuva420p");
|
||||
|
@ -3527,8 +3619,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
}
|
||||
|
@ -3702,15 +3794,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale=s=1280x720,format=yuva420p,hwupload
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=yuva420p");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
// alphasrc=s=1280x720:r=10:start=0,format=yuva420p,subtitles,hwupload
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=yuva420p");
|
||||
|
@ -3727,8 +3821,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
}
|
||||
|
@ -3938,16 +4032,18 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale,format=bgra,hwupload
|
||||
// overlay_qsv can handle overlay scaling,
|
||||
// add a dummy scale filter to pair with -canvas_size.
|
||||
subFilters.Add("scale=flags=fast_bilinear");
|
||||
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
|
@ -3973,8 +4069,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
}
|
||||
|
@ -4158,12 +4254,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
subFilters.Add("scale=flags=fast_bilinear");
|
||||
// overlay_qsv can handle overlay scaling, setup a smaller height to reduce transfer overhead
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
|
@ -4189,8 +4290,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
}
|
||||
|
@ -4425,12 +4526,17 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
subFilters.Add("scale=flags=fast_bilinear");
|
||||
// overlay_vaapi can handle overlay scaling, setup a smaller height to reduce transfer overhead
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, 1080);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, hasAssSubs ? 10 : 5);
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, 1080, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
|
@ -4454,8 +4560,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
|
||||
if (isVaapiEncoder)
|
||||
|
@ -4599,14 +4705,16 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
// scale=s=1280x720,format=bgra,hwupload
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, hasAssSubs ? 10 : 5);
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
|
@ -4815,8 +4923,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subSwScaleFilter);
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
|
||||
if (isVaapiEncoder)
|
||||
|
@ -4898,6 +5006,237 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return (newfilters, swFilterChain.SubFilters, swFilterChain.OverlayFilters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter of Rockchip RKMPP/RKRGA filter chain.
|
||||
/// </summary>
|
||||
/// <param name="state">Encoding state.</param>
|
||||
/// <param name="options">Encoding options.</param>
|
||||
/// <param name="vidEncoder">Video encoder to use.</param>
|
||||
/// <returns>The tuple contains three lists: main, sub and overlay filters.</returns>
|
||||
public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetRkmppVidFilterChain(
|
||||
EncodingJobInfo state,
|
||||
EncodingOptions options,
|
||||
string vidEncoder)
|
||||
{
|
||||
if (!string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
var isLinux = OperatingSystem.IsLinux();
|
||||
var vidDecoder = GetHardwareVideoDecoder(state, options) ?? string.Empty;
|
||||
var isSwDecoder = string.IsNullOrEmpty(vidDecoder);
|
||||
var isSwEncoder = !vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
|
||||
var isRkmppOclSupported = isLinux && IsRkmppFullSupported() && IsOpenclFullSupported();
|
||||
|
||||
if ((isSwDecoder && isSwEncoder)
|
||||
|| !isRkmppOclSupported
|
||||
|| !_mediaEncoder.SupportsFilter("alphasrc"))
|
||||
{
|
||||
return GetSwVidFilterChain(state, options, vidEncoder);
|
||||
}
|
||||
|
||||
// prefered rkmpp + rkrga + opencl filters pipeline
|
||||
if (isRkmppOclSupported)
|
||||
{
|
||||
return GetRkmppVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||
}
|
||||
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
public (List<string> MainFilters, List<string> SubFilters, List<string> OverlayFilters) GetRkmppVidFiltersPrefered(
|
||||
EncodingJobInfo state,
|
||||
EncodingOptions options,
|
||||
string vidDecoder,
|
||||
string vidEncoder)
|
||||
{
|
||||
var inW = state.VideoStream?.Width;
|
||||
var inH = state.VideoStream?.Height;
|
||||
var reqW = state.BaseRequest.Width;
|
||||
var reqH = state.BaseRequest.Height;
|
||||
var reqMaxW = state.BaseRequest.MaxWidth;
|
||||
var reqMaxH = state.BaseRequest.MaxHeight;
|
||||
var threeDFormat = state.MediaSource.Video3DFormat;
|
||||
|
||||
var isRkmppDecoder = vidDecoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
|
||||
var isRkmppEncoder = vidEncoder.Contains("rkmpp", StringComparison.OrdinalIgnoreCase);
|
||||
var isSwDecoder = !isRkmppDecoder;
|
||||
var isSwEncoder = !isRkmppEncoder;
|
||||
var isDrmInDrmOut = isRkmppDecoder && isRkmppEncoder;
|
||||
|
||||
var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true);
|
||||
var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true);
|
||||
var doDeintH2645 = doDeintH264 || doDeintHevc;
|
||||
var doOclTonemap = IsHwTonemapAvailable(state, options);
|
||||
|
||||
var hasSubs = state.SubtitleStream != null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode;
|
||||
var hasTextSubs = hasSubs && state.SubtitleStream.IsTextSubtitleStream;
|
||||
var hasGraphicalSubs = hasSubs && !state.SubtitleStream.IsTextSubtitleStream;
|
||||
var hasAssSubs = hasSubs
|
||||
&& (string.Equals(state.SubtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(state.SubtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/* Make main filters for video stream */
|
||||
var mainFilters = new List<string>();
|
||||
|
||||
mainFilters.Add(GetOverwriteColorPropertiesParam(state, doOclTonemap));
|
||||
|
||||
if (isSwDecoder)
|
||||
{
|
||||
// INPUT sw surface(memory)
|
||||
// sw deint
|
||||
if (doDeintH2645)
|
||||
{
|
||||
var swDeintFilter = GetSwDeinterlaceFilter(state, options);
|
||||
mainFilters.Add(swDeintFilter);
|
||||
}
|
||||
|
||||
var outFormat = doOclTonemap ? "yuv420p10le" : (hasGraphicalSubs ? "yuv420p" : "nv12");
|
||||
var swScaleFilter = GetSwScaleFilter(state, options, vidEncoder, inW, inH, threeDFormat, reqW, reqH, reqMaxW, reqMaxH);
|
||||
if (!string.IsNullOrEmpty(swScaleFilter))
|
||||
{
|
||||
swScaleFilter += ":flags=fast_bilinear";
|
||||
}
|
||||
|
||||
// sw scale
|
||||
mainFilters.Add(swScaleFilter);
|
||||
mainFilters.Add("format=" + outFormat);
|
||||
|
||||
// keep video at memory except ocl tonemap,
|
||||
// since the overhead caused by hwupload >>> using sw filter.
|
||||
// sw => hw
|
||||
if (doOclTonemap)
|
||||
{
|
||||
mainFilters.Add("hwupload=derive_device=opencl");
|
||||
}
|
||||
}
|
||||
else if (isRkmppDecoder)
|
||||
{
|
||||
// INPUT rkmpp/drm surface(gem/dma-heap)
|
||||
|
||||
var isFullAfbcPipeline = isDrmInDrmOut && !doOclTonemap;
|
||||
var outFormat = doOclTonemap ? "p010" : "nv12";
|
||||
var hwScaleFilter = GetHwScaleFilter("rkrga", outFormat, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
var hwScaleFilter2 = GetHwScaleFilter("rkrga", string.Empty, inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
|
||||
if (!hasSubs
|
||||
|| !isFullAfbcPipeline
|
||||
|| !string.IsNullOrEmpty(hwScaleFilter2))
|
||||
{
|
||||
// try enabling AFBC to save DDR bandwidth
|
||||
if (!string.IsNullOrEmpty(hwScaleFilter) && isFullAfbcPipeline)
|
||||
{
|
||||
hwScaleFilter += ":afbc=1";
|
||||
}
|
||||
|
||||
// hw scale
|
||||
mainFilters.Add(hwScaleFilter);
|
||||
}
|
||||
}
|
||||
|
||||
if (doOclTonemap && isRkmppDecoder)
|
||||
{
|
||||
// map from rkmpp/drm to opencl via drm-opencl interop.
|
||||
mainFilters.Add("hwmap=derive_device=opencl:mode=read");
|
||||
}
|
||||
|
||||
// ocl tonemap
|
||||
if (doOclTonemap)
|
||||
{
|
||||
var tonemapFilter = GetHwTonemapFilter(options, "opencl", "nv12");
|
||||
// enable tradeoffs for performance
|
||||
if (!string.IsNullOrEmpty(tonemapFilter))
|
||||
{
|
||||
tonemapFilter += ":tradeoff=1";
|
||||
}
|
||||
|
||||
mainFilters.Add(tonemapFilter);
|
||||
}
|
||||
|
||||
var memoryOutput = false;
|
||||
var isUploadForOclTonemap = isSwDecoder && doOclTonemap;
|
||||
if ((isRkmppDecoder && isSwEncoder) || isUploadForOclTonemap)
|
||||
{
|
||||
memoryOutput = true;
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
mainFilters.Add("hwdownload");
|
||||
mainFilters.Add("format=nv12");
|
||||
}
|
||||
|
||||
// OUTPUT nv12 surface(memory)
|
||||
if (isSwDecoder && isRkmppEncoder)
|
||||
{
|
||||
memoryOutput = true;
|
||||
}
|
||||
|
||||
if (memoryOutput)
|
||||
{
|
||||
// text subtitles
|
||||
if (hasTextSubs)
|
||||
{
|
||||
var textSubtitlesFilter = GetTextSubtitlesFilter(state, false, false);
|
||||
mainFilters.Add(textSubtitlesFilter);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDrmInDrmOut)
|
||||
{
|
||||
if (doOclTonemap)
|
||||
{
|
||||
// OUTPUT drm(nv12) surface(gem/dma-heap)
|
||||
// reverse-mapping via drm-opencl interop.
|
||||
mainFilters.Add("hwmap=derive_device=rkmpp:mode=write:reverse=1");
|
||||
mainFilters.Add("format=drm_prime");
|
||||
}
|
||||
}
|
||||
|
||||
/* Make sub and overlay filters for subtitle stream */
|
||||
var subFilters = new List<string>();
|
||||
var overlayFilters = new List<string>();
|
||||
if (isDrmInDrmOut)
|
||||
{
|
||||
if (hasSubs)
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
subFilters.Add("format=bgra");
|
||||
}
|
||||
else if (hasTextSubs)
|
||||
{
|
||||
var framerate = state.VideoStream?.RealFrameRate;
|
||||
var subFramerate = hasAssSubs ? Math.Min(framerate ?? 25, 60) : 10;
|
||||
|
||||
// alphasrc=s=1280x720:r=10:start=0,format=bgra,subtitles,hwupload
|
||||
var alphaSrcFilter = GetAlphaSrcFilter(state, inW, inH, reqW, reqH, reqMaxW, reqMaxH, subFramerate);
|
||||
var subTextSubtitlesFilter = GetTextSubtitlesFilter(state, true, true);
|
||||
subFilters.Add(alphaSrcFilter);
|
||||
subFilters.Add("format=bgra");
|
||||
subFilters.Add(subTextSubtitlesFilter);
|
||||
}
|
||||
|
||||
subFilters.Add("hwupload=derive_device=rkmpp");
|
||||
|
||||
// try enabling AFBC to save DDR bandwidth
|
||||
overlayFilters.Add("overlay_rkrga=eof_action=pass:repeatlast=0:format=nv12:afbc=1");
|
||||
}
|
||||
}
|
||||
else if (memoryOutput)
|
||||
{
|
||||
if (hasGraphicalSubs)
|
||||
{
|
||||
var subPreProcFilters = GetGraphicalSubPreProcessFilters(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
|
||||
subFilters.Add(subPreProcFilters);
|
||||
overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
|
||||
}
|
||||
}
|
||||
|
||||
return (mainFilters, subFilters, overlayFilters);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter of video processing filters.
|
||||
/// </summary>
|
||||
|
@ -4944,6 +5283,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
(mainFilters, subFilters, overlayFilters) = GetAppleVidFilterChain(state, options, outputVideoCodec);
|
||||
}
|
||||
else if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
(mainFilters, subFilters, overlayFilters) = GetRkmppVidFilterChain(state, options, outputVideoCodec);
|
||||
}
|
||||
else
|
||||
{
|
||||
(mainFilters, subFilters, overlayFilters) = GetSwVidFilterChain(state, options, outputVideoCodec);
|
||||
|
@ -5075,18 +5418,21 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|
||||
if (string.Equals(videoStream.PixelFormat, "yuv420p", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuvj420p", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv422p", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv444p", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.PixelFormat, "yuv420p10le", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv422p10le", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv444p10le", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 10;
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.PixelFormat, "yuv420p12le", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv422p12le", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.PixelFormat, "yuv444p12le", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return 12;
|
||||
|
@ -5139,7 +5485,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
|| string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return null;
|
||||
// One exception is that RKMPP decoder can handle H.264 High 10.
|
||||
if (!(string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(options.HardwareAccelerationType, "qsv", StringComparison.OrdinalIgnoreCase))
|
||||
|
@ -5166,6 +5517,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
{
|
||||
return GetVideotoolboxVidDecoder(state, options, videoStream, bitDepth);
|
||||
}
|
||||
|
||||
if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetRkmppVidDecoder(state, options, videoStream, bitDepth);
|
||||
}
|
||||
}
|
||||
|
||||
var whichCodec = videoStream.Codec;
|
||||
|
@ -5231,6 +5587,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return null;
|
||||
}
|
||||
|
||||
if (string.Equals(decoderSuffix, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return isCodecAvailable ? (" -c:v " + decoderName) : null;
|
||||
}
|
||||
|
||||
|
@ -5253,6 +5614,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
var isCudaSupported = (isLinux || isWindows) && IsCudaFullSupported();
|
||||
var isQsvSupported = (isLinux || isWindows) && _mediaEncoder.SupportsHwaccel("qsv");
|
||||
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
|
||||
var isRkmppSupported = isLinux && IsRkmppFullSupported();
|
||||
var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var ffmpegVersion = _mediaEncoder.EncoderVersion;
|
||||
|
@ -5355,6 +5717,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return " -hwaccel videotoolbox" + (outputHwSurface ? " -hwaccel_output_format videotoolbox_vld" : string.Empty);
|
||||
}
|
||||
|
||||
// Rockchip rkmpp
|
||||
if (string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase)
|
||||
&& isRkmppSupported
|
||||
&& isCodecAvailable)
|
||||
{
|
||||
return " -hwaccel rkmpp" + (outputHwSurface ? " -hwaccel_output_format drm_prime" : string.Empty);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -5661,6 +6031,102 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return null;
|
||||
}
|
||||
|
||||
public string GetRkmppVidDecoder(EncodingJobInfo state, EncodingOptions options, MediaStream videoStream, int bitDepth)
|
||||
{
|
||||
var isLinux = OperatingSystem.IsLinux();
|
||||
|
||||
if (!isLinux
|
||||
|| !string.Equals(options.HardwareAccelerationType, "rkmpp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var inW = state.VideoStream?.Width;
|
||||
var inH = state.VideoStream?.Height;
|
||||
var reqW = state.BaseRequest.Width;
|
||||
var reqH = state.BaseRequest.Height;
|
||||
var reqMaxW = state.BaseRequest.MaxWidth;
|
||||
var reqMaxH = state.BaseRequest.MaxHeight;
|
||||
|
||||
// rkrga RGA2e supports range from 1/16 to 16
|
||||
if (!IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 16.0f))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var isRkmppOclSupported = IsRkmppFullSupported() && IsOpenclFullSupported();
|
||||
var hwSurface = isRkmppOclSupported
|
||||
&& _mediaEncoder.SupportsFilter("alphasrc");
|
||||
|
||||
// rkrga RGA3 supports range from 1/8 to 8
|
||||
var isAfbcSupported = hwSurface && IsScaleRatioSupported(inW, inH, reqW, reqH, reqMaxW, reqMaxH, 8.0f);
|
||||
|
||||
// TODO: add more 8/10bit and 4:2:2 formats for Rkmpp after finishing the ffcheck tool
|
||||
var is8bitSwFormatsRkmpp = string.Equals("yuv420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||
var is10bitSwFormatsRkmpp = string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
|
||||
var is8_10bitSwFormatsRkmpp = is8bitSwFormatsRkmpp || is10bitSwFormatsRkmpp;
|
||||
|
||||
// nv15 and nv20 are bit-stream only formats
|
||||
if (is10bitSwFormatsRkmpp && !hwSurface)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is8bitSwFormatsRkmpp)
|
||||
{
|
||||
if (string.Equals(videoStream.Codec, "mpeg1video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "mpeg1video", bitDepth, hwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "mpeg2video", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "mpeg2video", bitDepth, hwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "mpeg4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "mpeg4", bitDepth, hwSurface);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "vp8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "vp8", bitDepth, hwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
if (is8_10bitSwFormatsRkmpp)
|
||||
{
|
||||
if (string.Equals(videoStream.Codec, "avc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "h264", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "hevc", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(videoStream.Codec, "h265", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "hevc", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "vp9", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var accelType = GetHwaccelType(state, options, "vp9", bitDepth, hwSurface);
|
||||
return accelType + ((!string.IsNullOrEmpty(accelType) && isAfbcSupported) ? " -afbc rga" : string.Empty);
|
||||
}
|
||||
|
||||
if (string.Equals(videoStream.Codec, "av1", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetHwaccelType(state, options, "av1", bitDepth, hwSurface);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of threads.
|
||||
/// </summary>
|
||||
|
@ -6075,13 +6541,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return " -codec:s:0 " + codec + " -disposition:s:0 default";
|
||||
}
|
||||
|
||||
public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string outputPath, string defaultPreset)
|
||||
public string GetProgressiveVideoFullCommandLine(EncodingJobInfo state, EncodingOptions encodingOptions, string defaultPreset)
|
||||
{
|
||||
// Get the output codec name
|
||||
var videoCodec = GetVideoEncoder(state, encodingOptions);
|
||||
|
||||
var format = string.Empty;
|
||||
var keyFrame = string.Empty;
|
||||
var outputPath = state.OutputFilePath;
|
||||
|
||||
if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
|
||||
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
||||
|
|
|
@ -619,6 +619,26 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string[] GetRequestedCodecTags(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.CodecTag))
|
||||
{
|
||||
return BaseRequest.CodecTag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(codec))
|
||||
{
|
||||
var codectag = BaseRequest.GetOption(codec, "codectag");
|
||||
|
||||
if (!string.IsNullOrEmpty(codectag))
|
||||
{
|
||||
return codectag.Split(new[] { '|', ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
public string GetRequestedLevel(string codec)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BaseRequest.Level))
|
||||
|
|
|
@ -96,9 +96,10 @@ public interface ITranscodeManager
|
|||
public void OnTranscodeEndRequest(TranscodingJob job);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the transcoding lock.
|
||||
/// Transcoding lock.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The output path of the transcoded file.</param>
|
||||
/// <returns>A <see cref="SemaphoreSlim"/>.</returns>
|
||||
public SemaphoreSlim GetTranscodingLock(string outputPath);
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An <see cref="IDisposable"/>.</returns>
|
||||
ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
public class ImageEncodingOptions
|
||||
{
|
||||
public string InputPath { get; set; }
|
||||
|
||||
public int? Width { get; set; }
|
||||
|
||||
public int? Height { get; set; }
|
||||
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
public int? Quality { get; set; }
|
||||
|
||||
public string Format { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.MediaEncoding
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MediaEncoderHelpers.
|
||||
/// </summary>
|
||||
public static class MediaEncoderHelpers
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace MediaBrowser.Controller.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that a <see cref="IServerEntryPoint"/> should be invoked as a pre-startup task.
|
||||
/// </summary>
|
||||
public interface IRunBeforeStartup
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Plugins
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an entry point for a module in the application. This interface is scanned for automatically and
|
||||
/// provides a hook to initialize the module at application start.
|
||||
/// The entry point can additionally be flagged as a pre-startup task by implementing the
|
||||
/// <see cref="IRunBeforeStartup"/> interface.
|
||||
/// </summary>
|
||||
public interface IServerEntryPoint : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Run the initialization for this module. This method is invoked at application start.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task RunAsync();
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -22,7 +23,7 @@ using Microsoft.Extensions.Logging;
|
|||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
public sealed class AttachmentExtractor : IAttachmentExtractor
|
||||
public sealed class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
{
|
||||
private readonly ILogger<AttachmentExtractor> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
@ -30,8 +31,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
|
||||
{
|
||||
o.PoolSize = 20;
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
|
@ -84,11 +88,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!Directory.Exists(outputPath))
|
||||
{
|
||||
|
@ -99,10 +99,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExtractAllAttachmentsExternal(
|
||||
|
@ -111,11 +107,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(Path.Join(outputPath, id)))
|
||||
{
|
||||
|
@ -131,10 +123,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllAttachmentsInternal(
|
||||
|
@ -256,11 +244,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = _semaphoreLocks.GetOrAdd(outputPath, key => new SemaphoreSlim(1, 1));
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
|
@ -271,10 +255,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAttachmentInternal(
|
||||
|
@ -379,5 +359,11 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
|||
var prefix = filename.AsSpan(0, 1);
|
||||
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_semaphoreLocks.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
"mpeg4_cuvid",
|
||||
"vp8_cuvid",
|
||||
"vp9_cuvid",
|
||||
"av1_cuvid"
|
||||
"av1_cuvid",
|
||||
"h264_rkmpp",
|
||||
"hevc_rkmpp",
|
||||
"mpeg1_rkmpp",
|
||||
"mpeg2_rkmpp",
|
||||
"mpeg4_rkmpp",
|
||||
"vp8_rkmpp",
|
||||
"vp9_rkmpp",
|
||||
"av1_rkmpp"
|
||||
};
|
||||
|
||||
private static readonly string[] _requiredEncoders = new[]
|
||||
|
@ -82,7 +90,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
"av1_vaapi",
|
||||
"h264_v4l2m2m",
|
||||
"h264_videotoolbox",
|
||||
"hevc_videotoolbox"
|
||||
"hevc_videotoolbox",
|
||||
"h264_rkmpp",
|
||||
"hevc_rkmpp"
|
||||
};
|
||||
|
||||
private static readonly string[] _requiredFilters = new[]
|
||||
|
@ -116,9 +126,12 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
"libplacebo",
|
||||
"scale_vulkan",
|
||||
"overlay_vulkan",
|
||||
"hwupload_vaapi",
|
||||
// videotoolbox
|
||||
"yadif_videotoolbox"
|
||||
"yadif_videotoolbox",
|
||||
// rkrga
|
||||
"scale_rkrga",
|
||||
"vpp_rkrga",
|
||||
"overlay_rkrga"
|
||||
};
|
||||
|
||||
private static readonly Dictionary<int, string[]> _filterOptionsDict = new Dictionary<int, string[]>
|
||||
|
|
|
@ -11,6 +11,7 @@ using System.Text.Json;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
|
@ -60,7 +61,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
private readonly IServerConfigurationManager _serverConfig;
|
||||
private readonly string _startupOptionFFmpegPath;
|
||||
|
||||
private readonly SemaphoreSlim _thumbnailResourcePool;
|
||||
private readonly AsyncNonKeyedLocker _thumbnailResourcePool;
|
||||
|
||||
private readonly object _runningProcessesLock = new object();
|
||||
private readonly List<ProcessWrapper> _runningProcesses = new List<ProcessWrapper>();
|
||||
|
@ -116,7 +117,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
_jsonSerializerOptions.Converters.Add(new JsonBoolStringConverter());
|
||||
|
||||
var semaphoreCount = 2 * Environment.ProcessorCount;
|
||||
_thumbnailResourcePool = new SemaphoreSlim(semaphoreCount, semaphoreCount);
|
||||
_thumbnailResourcePool = new(semaphoreCount);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -754,8 +755,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
bool ranToCompletion;
|
||||
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
|
||||
|
@ -776,10 +776,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
ranToCompletion = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
var file = _fileSystem.GetFileInfo(tempExtractPath);
|
||||
|
@ -908,8 +904,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
{
|
||||
bool ranToCompletion = false;
|
||||
|
||||
await _thumbnailResourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
using (await _thumbnailResourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
StartProcess(processWrapper);
|
||||
|
||||
|
@ -963,10 +958,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
StopProcess(processWrapper, 1000);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_thumbnailResourcePool.Release();
|
||||
}
|
||||
|
||||
var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
|
||||
|
||||
|
@ -1120,6 +1111,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return allVobs
|
||||
.Where(vob => titles.Contains(_fileSystem.GetFileNameWithoutExtension(vob).AsSpan().RightPart('_').ToString()))
|
||||
.Select(i => i.FullName)
|
||||
.Order()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -1136,6 +1128,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
return directoryFiles
|
||||
.Where(f => validPlaybackFiles.Contains(f.Name, StringComparer.OrdinalIgnoreCase))
|
||||
.Select(f => f.FullName)
|
||||
.Order()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -1159,31 +1152,29 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||
}
|
||||
|
||||
// Generate concat configuration entries for each file and write to file
|
||||
using (StreamWriter sw = new StreamWriter(concatFilePath))
|
||||
using StreamWriter sw = new StreamWriter(concatFilePath);
|
||||
foreach (var path in files)
|
||||
{
|
||||
foreach (var path in files)
|
||||
{
|
||||
var mediaInfoResult = GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
var mediaInfoResult = GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaType = DlnaProfileType.Video,
|
||||
MediaSource = new MediaSourceInfo
|
||||
{
|
||||
MediaType = DlnaProfileType.Video,
|
||||
MediaSource = new MediaSourceInfo
|
||||
{
|
||||
Path = path,
|
||||
Protocol = MediaProtocol.File,
|
||||
VideoType = videoType
|
||||
}
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
Path = path,
|
||||
Protocol = MediaProtocol.File,
|
||||
VideoType = videoType
|
||||
}
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
|
||||
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
||||
var duration = TimeSpan.FromTicks(mediaInfoResult.RunTimeTicks.Value).TotalSeconds;
|
||||
|
||||
// Add file path stanza to concat configuration
|
||||
sw.WriteLine("file '{0}'", path);
|
||||
// Add file path stanza to concat configuration
|
||||
sw.WriteLine("file '{0}'", path);
|
||||
|
||||
// Add duration stanza to concat configuration
|
||||
sw.WriteLine("duration {0}", duration);
|
||||
}
|
||||
// Add duration stanza to concat configuration
|
||||
sw.WriteLine("duration {0}", duration);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
|
||||
<PropertyGroup>
|
||||
|
@ -22,6 +22,7 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AsyncKeyedLock" />
|
||||
<PackageReference Include="BDInfo" />
|
||||
<PackageReference Include="libse" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" />
|
||||
|
|
|
@ -30,6 +30,8 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
private const string ArtistReplaceValue = " | ";
|
||||
|
||||
private readonly char[] _nameDelimiters = { '/', '|', ';', '\\' };
|
||||
private readonly string[] _webmVideoCodecs = { "av1", "vp8", "vp9" };
|
||||
private readonly string[] _webmAudioCodecs = { "opus", "vorbis" };
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly ILocalizationManager _localization;
|
||||
|
@ -114,7 +116,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
|
||||
if (data.Format is not null)
|
||||
{
|
||||
info.Container = NormalizeFormat(data.Format.FormatName);
|
||||
info.Container = NormalizeFormat(data.Format.FormatName, info.MediaStreams);
|
||||
|
||||
if (int.TryParse(data.Format.BitRate, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
|
@ -260,7 +262,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
return info;
|
||||
}
|
||||
|
||||
private string NormalizeFormat(string format)
|
||||
private string NormalizeFormat(string format, IReadOnlyList<MediaStream> mediaStreams)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(format))
|
||||
{
|
||||
|
@ -288,9 +290,20 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
{
|
||||
splitFormat[i] = "mkv";
|
||||
}
|
||||
|
||||
// Handle WebM
|
||||
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Limit WebM to supported codecs
|
||||
if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
|
||||
|| (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
splitFormat[i] = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(',', splitFormat);
|
||||
return string.Join(',', splitFormat.Where(s => !string.IsNullOrEmpty(s)));
|
||||
}
|
||||
|
||||
private int? GetEstimatedAudioBitrate(string codec, int? channels)
|
||||
|
@ -742,6 +755,10 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||
stream.LocalizedExternal = _localization.GetLocalizedString("External");
|
||||
stream.LocalizedHearingImpaired = _localization.GetLocalizedString("HearingImpaired");
|
||||
|
||||
// Graphical subtitle may have width and height info
|
||||
stream.Width = streamInfo.Width;
|
||||
stream.Height = streamInfo.Height;
|
||||
|
||||
if (string.IsNullOrEmpty(stream.Title))
|
||||
{
|
||||
// mp4 missing track title workaround: fall back to handler_name if populated and not the default "SubtitleHandler"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
@ -11,6 +11,7 @@ using System.Net.Http;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
|
@ -27,7 +28,7 @@ using UtfUnknown;
|
|||
|
||||
namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
{
|
||||
public sealed class SubtitleEncoder : ISubtitleEncoder
|
||||
public sealed class SubtitleEncoder : ISubtitleEncoder, IDisposable
|
||||
{
|
||||
private readonly ILogger<SubtitleEncoder> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
|
@ -40,8 +41,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
/// <summary>
|
||||
/// The _semaphoreLocks.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
private readonly AsyncKeyedLocker<string> _semaphoreLocks = new(o =>
|
||||
{
|
||||
o.PoolSize = 20;
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
public SubtitleEncoder(
|
||||
ILogger<SubtitleEncoder> logger,
|
||||
|
@ -194,36 +198,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
{
|
||||
if (!subtitleStream.IsExternal || subtitleStream.Path.EndsWith(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string outputFormat;
|
||||
string outputCodec;
|
||||
await ExtractAllTextSubtitles(mediaSource, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(subtitleStream.Codec, "srt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract
|
||||
outputCodec = "copy";
|
||||
outputFormat = subtitleStream.Codec;
|
||||
}
|
||||
else if (string.Equals(subtitleStream.Codec, "subrip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract
|
||||
outputCodec = "copy";
|
||||
outputFormat = "srt";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Extract
|
||||
outputCodec = "srt";
|
||||
outputFormat = "srt";
|
||||
}
|
||||
|
||||
// Extract
|
||||
var outputFormat = GetTextSubtitleFormat(subtitleStream);
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + outputFormat);
|
||||
|
||||
await ExtractTextSubtitle(mediaSource, subtitleStream, outputCodec, outputPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new SubtitleInfo()
|
||||
{
|
||||
Path = outputPath,
|
||||
|
@ -317,16 +296,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
throw new ArgumentException("Unsupported format: " + format);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock.
|
||||
/// </summary>
|
||||
/// <param name="filename">The filename.</param>
|
||||
/// <returns>System.Object.</returns>
|
||||
private SemaphoreSlim GetLock(string filename)
|
||||
{
|
||||
return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the text subtitle to SRT.
|
||||
/// </summary>
|
||||
|
@ -337,21 +306,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
/// <returns>Task.</returns>
|
||||
private async Task ConvertTextSubtitleToSrt(MediaStream subtitleStream, MediaSourceInfo mediaSource, string outputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = GetLock(outputPath);
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
await ConvertTextSubtitleToSrtInternal(subtitleStream, mediaSource, outputPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -467,6 +428,203 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
_logger.LogInformation("ffmpeg subtitle conversion succeeded for {Path}", inputPath);
|
||||
}
|
||||
|
||||
private string GetTextSubtitleFormat(MediaStream subtitleStream)
|
||||
{
|
||||
if (string.Equals(subtitleStream.Codec, "ass", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(subtitleStream.Codec, "ssa", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return subtitleStream.Codec;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "srt";
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCodecCopyable(string codec)
|
||||
{
|
||||
return string.Equals(codec, "ass", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "ssa", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "srt", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(codec, "subrip", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts all text subtitles.
|
||||
/// </summary>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
private async Task ExtractAllTextSubtitles(MediaSourceInfo mediaSource, CancellationToken cancellationToken)
|
||||
{
|
||||
var locks = new List<AsyncKeyedLockReleaser<string>>();
|
||||
var extractableStreams = new List<MediaStream>();
|
||||
|
||||
try
|
||||
{
|
||||
var subtitleStreams = mediaSource.MediaStreams
|
||||
.Where(stream => stream.IsTextSubtitleStream && stream.SupportsExternalStream);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
|
||||
|
||||
var @lock = _semaphoreLocks.GetOrAdd(outputPath);
|
||||
await @lock.SemaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (File.Exists(outputPath))
|
||||
{
|
||||
@lock.Dispose();
|
||||
continue;
|
||||
}
|
||||
|
||||
locks.Add(@lock);
|
||||
extractableStreams.Add(subtitleStream);
|
||||
}
|
||||
|
||||
if (extractableStreams.Count > 0)
|
||||
{
|
||||
await ExtractAllTextSubtitlesInternal(mediaSource, extractableStreams, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Unable to get streams for File:{File}", mediaSource.Path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var @lock in locks)
|
||||
{
|
||||
@lock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractAllTextSubtitlesInternal(
|
||||
MediaSourceInfo mediaSource,
|
||||
List<MediaStream> subtitleStreams,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var inputPath = mediaSource.Path;
|
||||
var outputPaths = new List<string>();
|
||||
var args = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i \"{0}\" -copyts",
|
||||
inputPath);
|
||||
|
||||
foreach (var subtitleStream in subtitleStreams)
|
||||
{
|
||||
var outputPath = GetSubtitleCachePath(mediaSource, subtitleStream.Index, "." + GetTextSubtitleFormat(subtitleStream));
|
||||
var outputCodec = IsCodecCopyable(subtitleStream.Codec) ? "copy" : "srt";
|
||||
var streamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
if (streamIndex == -1)
|
||||
{
|
||||
_logger.LogError("Cannot find subtitle stream index for {InputPath} ({Index}), skipping this stream", inputPath, subtitleStream.Index);
|
||||
continue;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new FileNotFoundException($"Calculated path ({outputPath}) is not valid."));
|
||||
|
||||
outputPaths.Add(outputPath);
|
||||
args += string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
" -map 0:{0} -an -vn -c:s {1} \"{2}\"",
|
||||
streamIndex,
|
||||
outputCodec,
|
||||
outputPath);
|
||||
}
|
||||
|
||||
int exitCode;
|
||||
|
||||
using (var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
FileName = _mediaEncoder.EncoderPath,
|
||||
Arguments = args,
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
ErrorDialog = false
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
})
|
||||
{
|
||||
_logger.LogInformation("{File} {Arguments}", process.StartInfo.FileName, process.StartInfo.Arguments);
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting ffmpeg");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
|
||||
if (exitCode == -1)
|
||||
{
|
||||
failed = true;
|
||||
|
||||
foreach (var outputPath in outputPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Deleting extracted subtitle due to failure: {Path}", outputPath);
|
||||
_fileSystem.DeleteFile(outputPath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting extracted subtitle {Path}", outputPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var outputPath in outputPaths)
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
_logger.LogError("ffmpeg subtitle extraction failed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
failed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (outputPath.EndsWith("ass", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await SetAssFont(outputPath, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_logger.LogInformation("ffmpeg subtitle extraction completed for {InputPath} to {OutputPath}", inputPath, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed)
|
||||
{
|
||||
throw new FfmpegException(
|
||||
string.Format(CultureInfo.InvariantCulture, "ffmpeg subtitle extraction failed for {0}", inputPath));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the text subtitle.
|
||||
/// </summary>
|
||||
|
@ -484,16 +642,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
string outputPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var semaphore = GetLock(outputPath);
|
||||
|
||||
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
try
|
||||
using (await _semaphoreLocks.LockAsync(outputPath, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!File.Exists(outputPath))
|
||||
{
|
||||
var subtitleStreamIndex = EncodingHelper.FindIndex(mediaSource.MediaStreams, subtitleStream);
|
||||
|
||||
var args = _mediaEncoder.GetInputArgument(mediaSource.Path, mediaSource);
|
||||
|
||||
if (subtitleStream.IsExternal)
|
||||
|
@ -509,10 +663,6 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExtractTextSubtitleInternal(
|
||||
|
@ -530,7 +680,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"-i {0} -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
"-i \"{0}\" -copyts -map 0:{1} -an -vn -c:s {2} \"{3}\"",
|
||||
inputPath,
|
||||
subtitleStreamIndex,
|
||||
outputCodec,
|
||||
|
@ -728,6 +878,12 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_semaphoreLocks.Dispose();
|
||||
}
|
||||
|
||||
#pragma warning disable CA1034 // Nested types should not be visible
|
||||
// Only public for the unit tests
|
||||
public readonly record struct SubtitleInfo
|
||||
|
|
|
@ -4,10 +4,12 @@ using System.Diagnostics;
|
|||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
|
@ -43,7 +45,11 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
private readonly IAttachmentExtractor _attachmentExtractor;
|
||||
|
||||
private readonly List<TranscodingJob> _activeTranscodingJobs = new();
|
||||
private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new();
|
||||
private readonly AsyncKeyedLocker<string> _transcodingLocks = new(o =>
|
||||
{
|
||||
o.PoolSize = 20;
|
||||
o.PoolInitialFill = 1;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TranscodeManager"/> class.
|
||||
|
@ -224,11 +230,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
lock (_transcodingLocks)
|
||||
{
|
||||
_transcodingLocks.Remove(job.Path!);
|
||||
}
|
||||
|
||||
job.Stop();
|
||||
|
||||
if (delete(job.Path!))
|
||||
|
@ -404,7 +405,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
|
||||
if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
|
||||
{
|
||||
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw new ArgumentException("User does not have access to video transcoding.");
|
||||
}
|
||||
|
@ -416,7 +417,12 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
if (state.SubtitleStream is not null && state.SubtitleDeliveryMethod == SubtitleDeliveryMethod.Encode)
|
||||
{
|
||||
var attachmentPath = Path.Combine(_appPaths.CachePath, "attachments", state.MediaSource.Id);
|
||||
if (state.VideoType != VideoType.Dvd)
|
||||
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
|
||||
{
|
||||
var concatPath = Path.Join(_serverConfigurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
|
||||
await _attachmentExtractor.ExtractAllAttachments(concatPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
@ -451,7 +457,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
var transcodingJob = this.OnTranscodeBeginning(
|
||||
var transcodingJob = OnTranscodeBeginning(
|
||||
outputPath,
|
||||
state.Request.PlaySessionId,
|
||||
state.MediaSource.LiveStreamId,
|
||||
|
@ -506,7 +512,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error starting FFmpeg");
|
||||
this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
OnTranscodeFailedToStart(outputPath, transcodingJobType, state);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
@ -625,11 +631,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
lock (_transcodingLocks)
|
||||
{
|
||||
_transcodingLocks.Remove(path);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(state.Request.DeviceId))
|
||||
{
|
||||
_sessionManager.ClearTranscodingInfo(state.Request.DeviceId);
|
||||
|
@ -705,21 +706,6 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SemaphoreSlim GetTranscodingLock(string outputPath)
|
||||
{
|
||||
lock (_transcodingLocks)
|
||||
{
|
||||
if (!_transcodingLocks.TryGetValue(outputPath, out SemaphoreSlim? result))
|
||||
{
|
||||
result = new SemaphoreSlim(1, 1);
|
||||
_transcodingLocks[outputPath] = result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlaybackProgress(object? sender, PlaybackProgressEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(e.PlaySessionId))
|
||||
|
@ -742,10 +728,23 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transcoding lock.
|
||||
/// </summary>
|
||||
/// <param name="outputPath">The output path of the transcoded file.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>An <see cref="IDisposable"/>.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ValueTask<IDisposable> LockAsync(string outputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
return _transcodingLocks.LockAsync(outputPath, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_sessionManager.PlaybackProgress -= OnPlaybackProgress;
|
||||
_sessionManager.PlaybackStart -= OnPlaybackProgress;
|
||||
_transcodingLocks.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue