Browse Source

Merge changes

Mark Cilia Vincenti 1 year ago
parent
commit
f26fc7dfb2
100 changed files with 1308 additions and 890 deletions
  1. 1 1
      .ci/azure-pipelines-package.yml
  2. 28 0
      .devcontainer/Dev - Server Ffmpeg/devcontainer.json
  3. 32 0
      .devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh
  4. 3 3
      .github/workflows/ci-codeql-analysis.yml
  5. 5 5
      .github/workflows/ci-openapi.yml
  6. 5 5
      .github/workflows/commands.yml
  7. 1 1
      .vscode/extensions.json
  8. 12 0
      .vscode/launch.json
  9. 4 1
      CONTRIBUTORS.md
  10. 2 2
      Directory.Packages.props
  11. 30 36
      Dockerfile
  12. 24 32
      Dockerfile.arm
  13. 26 28
      Dockerfile.arm64
  14. 1 1
      Emby.Server.Implementations/ApplicationHost.cs
  15. 16 16
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  16. 2 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  17. 15 15
      Emby.Server.Implementations/Library/LibraryManager.cs
  18. 10 7
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  19. 3 2
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  20. 2 1
      Emby.Server.Implementations/Library/MusicManager.cs
  21. 2 2
      Emby.Server.Implementations/Library/SearchEngine.cs
  22. 3 2
      Emby.Server.Implementations/Library/UserViewManager.cs
  23. 4 2
      Emby.Server.Implementations/Localization/Core/et.json
  24. 2 2
      Emby.Server.Implementations/Localization/Core/ka.json
  25. 1 0
      Emby.Server.Implementations/Localization/Core/ky.json
  26. 3 1
      Emby.Server.Implementations/Localization/Core/ms.json
  27. 10 10
      Emby.Server.Implementations/Localization/Core/nb.json
  28. 3 1
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  29. 2 2
      Emby.Server.Implementations/Localization/Core/sv.json
  30. 3 0
      Emby.Server.Implementations/Localization/Core/ur.json
  31. 3 1
      Emby.Server.Implementations/Localization/Core/vi.json
  32. 4 4
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  33. 2 1
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  34. 3 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
  35. 17 17
      Emby.Server.Implementations/Session/SessionManager.cs
  36. 2 1
      Emby.Server.Implementations/SyncPlay/Group.cs
  37. 4 3
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  38. 2 1
      Emby.Server.Implementations/Updates/InstallationManager.cs
  39. 2 1
      Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs
  40. 2 1
      Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs
  41. 4 3
      Jellyfin.Api/Controllers/ArtistsController.cs
  42. 3 2
      Jellyfin.Api/Controllers/ChannelsController.cs
  43. 3 2
      Jellyfin.Api/Controllers/FilterController.cs
  44. 3 2
      Jellyfin.Api/Controllers/GenresController.cs
  45. 8 7
      Jellyfin.Api/Controllers/InstantMixController.cs
  46. 3 2
      Jellyfin.Api/Controllers/ItemsController.cs
  47. 13 13
      Jellyfin.Api/Controllers/LibraryController.cs
  48. 27 27
      Jellyfin.Api/Controllers/LiveTvController.cs
  49. 2 1
      Jellyfin.Api/Controllers/MoviesController.cs
  50. 3 2
      Jellyfin.Api/Controllers/MusicGenresController.cs
  51. 3 2
      Jellyfin.Api/Controllers/PersonsController.cs
  52. 2 1
      Jellyfin.Api/Controllers/PlaylistsController.cs
  53. 1 1
      Jellyfin.Api/Controllers/SearchController.cs
  54. 4 3
      Jellyfin.Api/Controllers/SessionController.cs
  55. 3 2
      Jellyfin.Api/Controllers/StudiosController.cs
  56. 2 1
      Jellyfin.Api/Controllers/SuggestionsController.cs
  57. 5 5
      Jellyfin.Api/Controllers/TvShowsController.cs
  58. 2 1
      Jellyfin.Api/Controllers/UserController.cs
  59. 10 9
      Jellyfin.Api/Controllers/UserLibraryController.cs
  60. 4 3
      Jellyfin.Api/Controllers/VideosController.cs
  61. 3 3
      Jellyfin.Api/Controllers/YearsController.cs
  62. 2 1
      Jellyfin.Api/Helpers/MediaInfoHelper.cs
  63. 2 1
      Jellyfin.Api/Helpers/RequestHelpers.cs
  64. 1 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  65. 2 1
      Jellyfin.Server.Implementations/Users/UserManager.cs
  66. 0 9
      Jellyfin.Server/CoreAppHost.cs
  67. 2 0
      Jellyfin.Server/Startup.cs
  68. 0 7
      MediaBrowser.Controller/Channels/IChannelManager.cs
  69. 2 1
      MediaBrowser.Controller/Entities/AggregateFolder.cs
  70. 1 1
      MediaBrowser.Controller/Entities/Audio/MusicArtist.cs
  71. 8 8
      MediaBrowser.Controller/Entities/BaseItem.cs
  72. 5 4
      MediaBrowser.Controller/Entities/Folder.cs
  73. 6 5
      MediaBrowser.Controller/Entities/TV/Episode.cs
  74. 3 2
      MediaBrowser.Controller/Entities/TV/Season.cs
  75. 4 4
      MediaBrowser.Controller/Entities/UserView.cs
  76. 1 1
      MediaBrowser.Controller/Entities/UserViewBuilder.cs
  77. 1 1
      MediaBrowser.Controller/Entities/Video.cs
  78. 26 0
      MediaBrowser.Controller/LiveTv/IGuideManager.cs
  79. 1 20
      MediaBrowser.Controller/LiveTv/ILiveTvManager.cs
  80. 0 13
      MediaBrowser.Controller/LiveTv/ILiveTvService.cs
  81. 0 7
      MediaBrowser.Controller/LiveTv/ITunerHost.cs
  82. 46 0
      MediaBrowser.Controller/LiveTv/ITunerHostManager.cs
  83. 0 54
      MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs
  84. 0 77
      MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs
  85. 0 210
      MediaBrowser.Controller/LiveTv/RecordingInfo.cs
  86. 0 16
      MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs
  87. 526 60
      MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
  88. 2 1
      MediaBrowser.Controller/Session/ISessionManager.cs
  89. 7 21
      MediaBrowser.Controller/Session/SessionInfo.cs
  90. 17 4
      MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs
  91. 1 6
      MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
  92. 4 0
      MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
  93. 200 28
      MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
  94. 2 1
      MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs
  95. 2 1
      MediaBrowser.Model/Dlna/StreamBuilder.cs
  96. 0 2
      MediaBrowser.Model/IO/IStreamHelper.cs
  97. 0 12
      MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs
  98. 6 1
      MediaBrowser.Model/Session/HardwareEncodingType.cs
  99. 2 2
      MediaBrowser.Providers/Manager/ProviderManager.cs
  100. 22 4
      MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

+ 1 - 1
.ci/azure-pipelines-package.yml

@@ -259,7 +259,7 @@ jobs:
       publishFeedCredentials: 'NugetOrg'
       allowPackageConflicts: true # This ignores an error if the version already exists
 
-  - task: NuGetAuthenticate@0
+  - task: NuGetAuthenticate@1
     displayName: 'Authenticate to unstable Nuget feed'
     condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
 

+ 28 - 0
.devcontainer/Dev - Server Ffmpeg/devcontainer.json

@@ -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 - 0
.devcontainer/Dev - Server Ffmpeg/install-ffmpeg.sh

@@ -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

+ 3 - 3
.github/workflows/ci-codeql-analysis.yml

@@ -27,11 +27,11 @@ jobs:
         dotnet-version: '8.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+      uses: github/codeql-action/init@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+      uses: github/codeql-action/autobuild@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
+      uses: github/codeql-action/analyze@e8893c57a1f3a2b659b6b55564fdfdbbd2982911 # v3.24.0

+ 5 - 5
.github/workflows/ci-openapi.yml

@@ -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@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
+        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
         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@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
+        uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
         with:
           name: openapi-base
           retention-days: 14
@@ -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 }}

+ 5 - 5
.github/workflows/commands.yml

@@ -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 }}

+ 1 - 1
.vscode/extensions.json

@@ -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 - 0
.vscode/launch.json

@@ -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 - 1
CONTRIBUTORS.md

@@ -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)
@@ -173,9 +174,11 @@
  - [tallbl0nde](https://github.com/tallbl0nde)
  - [sleepycatcoding](https://github.com/sleepycatcoding)
  - [scampower3](https://github.com/scampower3)
- - [Chris-Codes-It] (https://github.com/Chris-Codes-It)
+ - [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)
 
 # Emby Contributors
 

+ 2 - 2
Directory.Packages.props

@@ -4,7 +4,7 @@
   </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.0" />
+    <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" />
@@ -88,4 +88,4 @@
     <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
     <PackageVersion Include="xunit" Version="2.6.5" />
   </ItemGroup>
-</Project>
+</Project>

+ 30 - 36
Dockerfile

@@ -8,72 +8,68 @@ FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && apk del curl \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm run build:production \
  && mv dist /dist
 
-FROM debian:stable-slim as app
+FROM debian:bookworm-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 # http://stackoverflow.com/questions/48162574/ddg#49462622
 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_VISIBLE_DEVICES="all"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
+ENV JELLYFIN_DATA_DIR=/config
+ENV JELLYFIN_CACHE_DIR=/cache
+
 # https://github.com/intel/compute-runtime/releases
-ARG GMMLIB_VERSION=22.0.2
-ARG IGC_VERSION=1.0.10395
-ARG NEO_VERSION=22.08.22549
-ARG LEVEL_ZERO_VERSION=1.3.22549
+ARG GMMLIB_VERSION=22.3.11.ci17757293
+ARG IGC_VERSION=1.0.15136.22
+ARG NEO_VERSION=23.39.27427.23
+ARG LEVEL_ZERO_VERSION=1.3.27427.23
 
-# Install dependencies:
-# mesa-va-drivers: needed for AMD VAAPI. Mesa >= 20.1 is required for HEVC transcoding.
-# curl: healthcheck
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg wget curl \
- && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
+ && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
  && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
  && apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y \
-   mesa-va-drivers \
-   jellyfin-ffmpeg5 \
-   openssl \
-   locales \
+ && apt-get install --no-install-recommends --no-install-suggests -y mesa-va-drivers jellyfin-ffmpeg6 openssl locales \
 # Intel VAAPI Tone mapping dependencies:
 # Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now.
 # Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled.
  && mkdir intel-compute-runtime \
  && cd intel-compute-runtime \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \
- && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
- && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
- && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
+ && curl -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \
+         -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \
+         -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \
+         -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \
+         -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/libigdgmm12_${GMMLIB_VERSION}_amd64.deb \
  && dpkg -i *.deb \
  && cd .. \
  && rm -rf intel-compute-runtime \
- && apt-get remove gnupg wget -y \
+ && apt-get remove gnupg -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
- && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media \
+ && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
+ && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
-# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+ENV LC_ALL=en_US.UTF-8
+ENV LANG=en_US.UTF-8
+ENV LANGUAGE=en_US:en
 
 FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# because of changes in docker and systemd we need to not build in parallel at the moment
-# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting
-RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
+
+RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
 
@@ -83,11 +79,9 @@ COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 
 EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
-    "--datadir", "/config", \
-    "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
+ENTRYPOINT [ "./jellyfin/jellyfin", \
+             "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
+    CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 24 - 32
Dockerfile.arm

@@ -4,64 +4,58 @@
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=8.0
 
-
 FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && apk del curl \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm run build:production \
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
-FROM arm32v7/debian:stable-slim as app
+FROM arm32v7/debian:bookworm-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 # http://stackoverflow.com/questions/48162574/ddg#49462622
 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_VISIBLE_DEVICES="all"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
+ENV JELLYFIN_DATA_DIR=/config
+ENV JELLYFIN_CACHE_DIR=/cache
+
 COPY --from=qemu /usr/bin/qemu-arm-static /usr/bin
 
-# curl: setup & healthcheck
 RUN apt-get update \
- && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl && \
- curl -ks https://repo.jellyfin.org/debian/jellyfin_team.gpg.key | apt-key add - && \
- curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \
- echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \
- echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \
- apt-get update && \
- apt-get install --no-install-recommends --no-install-suggests -y \
- jellyfin-ffmpeg \
- libssl-dev \
- libfontconfig1 \
- libfreetype6 \
- vainfo \
- libva2 \
- locales \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
+ && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
+ && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
+ && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
+ && apt-get update \
+ && apt-get install --no-install-recommends --no-install-suggests -y \
+    jellyfin-ffmpeg6 libssl-dev libfontconfig1 \
+    libfreetype6 vainfo libva2 locales \
  && apt-get remove gnupg -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
- && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media \
+ && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
+ && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
-# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+ENV LC_ALL=en_US.UTF-8
+ENV LANG=en_US.UTF-8
+ENV LANGUAGE=en_US:en
 
 FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
+
 RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
@@ -72,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 
 EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
-    "--datadir", "/config", \
-    "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
+VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
+ENTRYPOINT [ "/jellyfin/jellyfin", \
+             "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
+    CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 26 - 28
Dockerfile.arm64

@@ -4,58 +4,58 @@
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=8.0
 
-
 FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
+ && apk del curl \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm run build:production \
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
-FROM arm64v8/debian:stable-slim as app
+FROM arm64v8/debian:bookworm-slim as app
 
 # https://askubuntu.com/questions/972516/debian-frontend-environment-variable
 ARG DEBIAN_FRONTEND="noninteractive"
 # http://stackoverflow.com/questions/48162574/ddg#49462622
 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
 # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
+ENV NVIDIA_VISIBLE_DEVICES="all"
 ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility"
 
+ENV JELLYFIN_DATA_DIR=/config
+ENV JELLYFIN_CACHE_DIR=/cache
+
 COPY --from=qemu /usr/bin/qemu-aarch64-static /usr/bin
 
-# curl: healcheck
-RUN apt-get update && apt-get install --no-install-recommends --no-install-suggests -y \
- ffmpeg \
- libssl-dev \
- ca-certificates \
- libfontconfig1 \
- libfreetype6 \
- libomxil-bellagio0 \
- libomxil-bellagio-bin \
- locales \
- curl \
+RUN apt-get update \
+ && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \
+ && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \
+ && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \
+ && echo "deb [arch=$( dpkg --print-architecture )] https://repo.jellyfin.org/$( awk -F'=' '/^ID=/{ print $NF }' /etc/os-release ) $( awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release ) main" | tee /etc/apt/sources.list.d/jellyfin.list \
+ && apt-get update \
+ && apt-get install --no-install-recommends --no-install-suggests -y \
+    jellyfin-ffmpeg6 locales libssl-dev libfontconfig1 \
+    libfreetype6 libomxil-bellagio0 libomxil-bellagio-bin \
+ && apt-get remove gnupg -y \
  && apt-get clean autoclean -y \
  && apt-get autoremove -y \
  && rm -rf /var/lib/apt/lists/* \
- && mkdir -p /cache /config /media \
- && chmod 777 /cache /config /media \
+ && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
+ && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \
  && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen
 
-# ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
-ENV LC_ALL en_US.UTF-8
-ENV LANG en_US.UTF-8
-ENV LANGUAGE en_US:en
+ENV LC_ALL=en_US.UTF-8
+ENV LANG=en_US.UTF-8
+ENV LANGUAGE=en_US:en
 
 FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder
 WORKDIR /repo
 COPY . .
 ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
-# Discard objs - may cause failures if exists
-RUN find . -type d -name obj | xargs -r rm -r
-# Build
+
 RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none
 
 FROM app
@@ -66,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin
 COPY --from=web-builder /dist /jellyfin/jellyfin-web
 
 EXPOSE 8096
-VOLUME /cache /config
-ENTRYPOINT ["./jellyfin/jellyfin", \
-    "--datadir", "/config", \
-    "--cachedir", "/cache", \
-    "--ffmpeg", "/usr/bin/ffmpeg"]
+VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR}
+ENTRYPOINT [ "/jellyfin/jellyfin", \
+             "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1
+    CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 1 - 1
Emby.Server.Implementations/ApplicationHost.cs

@@ -695,7 +695,7 @@ namespace Emby.Server.Implementations
                 GetExports<IMetadataSaver>(),
                 GetExports<IExternalId>());
 
-            Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
+            Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<IListingsProvider>());
 
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
         }

+ 16 - 16
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -699,7 +699,7 @@ namespace Emby.Server.Implementations.Data
                 saveItemStatement.TryBindNull("@EndDate");
             }
 
-            saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
+            saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture));
 
             if (item is IHasProgramAttributes hasProgramAttributes)
             {
@@ -729,7 +729,7 @@ namespace Emby.Server.Implementations.Data
             saveItemStatement.TryBind("@ProductionYear", item.ProductionYear);
 
             var parentId = item.ParentId;
-            if (parentId.Equals(default))
+            if (parentId.IsEmpty())
             {
                 saveItemStatement.TryBindNull("@ParentId");
             }
@@ -925,7 +925,7 @@ namespace Emby.Server.Implementations.Data
             {
                 saveItemStatement.TryBind("@SeasonName", episode.SeasonName);
 
-                var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId;
+                var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId;
 
                 saveItemStatement.TryBind("@SeasonId", nullableSeasonId);
             }
@@ -937,7 +937,7 @@ namespace Emby.Server.Implementations.Data
 
             if (item is IHasSeries hasSeries)
             {
-                var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId;
+                var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId;
 
                 saveItemStatement.TryBind("@SeriesId", nullableSeriesId);
                 saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey);
@@ -1010,7 +1010,7 @@ namespace Emby.Server.Implementations.Data
             }
 
             Guid ownerId = item.OwnerId;
-            if (ownerId.Equals(default))
+            if (ownerId.IsEmpty())
             {
                 saveItemStatement.TryBindNull("@OwnerId");
             }
@@ -1266,7 +1266,7 @@ namespace Emby.Server.Implementations.Data
         /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception>
         public BaseItem RetrieveItem(Guid id)
         {
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
@@ -1970,7 +1970,7 @@ namespace Emby.Server.Implementations.Data
         {
             CheckDisposed();
 
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(id));
             }
@@ -3230,7 +3230,7 @@ namespace Emby.Server.Implementations.Data
                 whereClauses.Add($"ChannelId in ({inClause})");
             }
 
-            if (!query.ParentId.Equals(default))
+            if (!query.ParentId.IsEmpty())
             {
                 whereClauses.Add("ParentId=@ParentId");
                 statement?.TryBind("@ParentId", query.ParentId);
@@ -4452,7 +4452,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
 
         public void DeleteItem(Guid id)
         {
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(id));
             }
@@ -4583,13 +4583,13 @@ AND Type = @InternalPersonType)");
                 statement?.TryBind("@UserId", query.User.InternalId);
             }
 
-            if (!query.ItemId.Equals(default))
+            if (!query.ItemId.IsEmpty())
             {
                 whereClauses.Add("ItemId=@ItemId");
                 statement?.TryBind("@ItemId", query.ItemId);
             }
 
-            if (!query.AppearsInItemId.Equals(default))
+            if (!query.AppearsInItemId.IsEmpty())
             {
                 whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)");
                 statement?.TryBind("@AppearsInItemId", query.AppearsInItemId);
@@ -4640,7 +4640,7 @@ AND Type = @InternalPersonType)");
 
         private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement)
         {
-            if (itemId.Equals(default))
+            if (itemId.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(itemId));
             }
@@ -5156,7 +5156,7 @@ AND Type = @InternalPersonType)");
 
         private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db)
         {
-            if (itemId.Equals(default))
+            if (itemId.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(itemId));
             }
@@ -5228,7 +5228,7 @@ AND Type = @InternalPersonType)");
 
         public void UpdatePeople(Guid itemId, List<PersonInfo> people)
         {
-            if (itemId.Equals(default))
+            if (itemId.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(itemId));
             }
@@ -5378,7 +5378,7 @@ AND Type = @InternalPersonType)");
         {
             CheckDisposed();
 
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(id));
             }
@@ -5758,7 +5758,7 @@ AND Type = @InternalPersonType)");
             CancellationToken cancellationToken)
         {
             CheckDisposed();
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentException("Guid can't be empty.", nameof(id));
             }

+ 2 - 1
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -7,6 +7,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
@@ -241,7 +242,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint
     {
         var userIds = _sessionManager.Sessions
             .Select(i => i.UserId)
-            .Where(i => !i.Equals(default))
+            .Where(i => !i.IsEmpty())
             .Distinct()
             .ToArray();
 

+ 15 - 15
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -732,7 +732,7 @@ namespace Emby.Server.Implementations.Library
                 Path = path
             };
 
-            if (folder.Id.Equals(default))
+            if (folder.Id.IsEmpty())
             {
                 if (string.IsNullOrEmpty(folder.Path))
                 {
@@ -1219,7 +1219,7 @@ namespace Emby.Server.Implementations.Library
         /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception>
         public BaseItem GetItemById(Guid id)
         {
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
@@ -1241,7 +1241,7 @@ namespace Emby.Server.Implementations.Library
 
         public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent)
         {
-            if (query.Recursive && !query.ParentId.Equals(default))
+            if (query.Recursive && !query.ParentId.IsEmpty())
             {
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
@@ -1272,7 +1272,7 @@ namespace Emby.Server.Implementations.Library
 
         public int GetCount(InternalItemsQuery query)
         {
-            if (query.Recursive && !query.ParentId.Equals(default))
+            if (query.Recursive && !query.ParentId.IsEmpty())
             {
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
@@ -1430,7 +1430,7 @@ namespace Emby.Server.Implementations.Library
 
         public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query)
         {
-            if (query.Recursive && !query.ParentId.Equals(default))
+            if (query.Recursive && !query.ParentId.IsEmpty())
             {
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
@@ -1486,7 +1486,7 @@ namespace Emby.Server.Implementations.Library
         private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true)
         {
             if (query.AncestorIds.Length == 0 &&
-                query.ParentId.Equals(default) &&
+                query.ParentId.IsEmpty() &&
                 query.ChannelIds.Count == 0 &&
                 query.TopParentIds.Length == 0 &&
                 string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) &&
@@ -1520,7 +1520,7 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 // Translate view into folders
-                if (!view.DisplayParentId.Equals(default))
+                if (!view.DisplayParentId.IsEmpty())
                 {
                     var displayParent = GetItemById(view.DisplayParentId);
                     if (displayParent is not null)
@@ -1531,7 +1531,7 @@ namespace Emby.Server.Implementations.Library
                     return Array.Empty<Guid>();
                 }
 
-                if (!view.ParentId.Equals(default))
+                if (!view.ParentId.IsEmpty())
                 {
                     var displayParent = GetItemById(view.ParentId);
                     if (displayParent is not null)
@@ -2137,7 +2137,7 @@ namespace Emby.Server.Implementations.Library
                 return null;
             }
 
-            while (!item.ParentId.Equals(default))
+            while (!item.ParentId.IsEmpty())
             {
                 var parent = item.GetParent();
                 if (parent is null || parent is AggregateFolder)
@@ -2215,7 +2215,7 @@ namespace Emby.Server.Implementations.Library
             CollectionType? viewType,
             string sortName)
         {
-            var parentIdString = parentId.Equals(default)
+            var parentIdString = parentId.IsEmpty()
                 ? null
                 : parentId.ToString("N", CultureInfo.InvariantCulture);
             var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
@@ -2251,7 +2251,7 @@ namespace Emby.Server.Implementations.Library
 
             var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 
-            if (!refresh && !item.DisplayParentId.Equals(default))
+            if (!refresh && !item.DisplayParentId.IsEmpty())
             {
                 var displayParent = GetItemById(item.DisplayParentId);
                 refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2315,7 +2315,7 @@ namespace Emby.Server.Implementations.Library
 
             var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 
-            if (!refresh && !item.DisplayParentId.Equals(default))
+            if (!refresh && !item.DisplayParentId.IsEmpty())
             {
                 var displayParent = GetItemById(item.DisplayParentId);
                 refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2345,7 +2345,7 @@ namespace Emby.Server.Implementations.Library
         {
             ArgumentException.ThrowIfNullOrEmpty(name);
 
-            var parentIdString = parentId.Equals(default)
+            var parentIdString = parentId.IsEmpty()
                 ? null
                 : parentId.ToString("N", CultureInfo.InvariantCulture);
             var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
@@ -2391,7 +2391,7 @@ namespace Emby.Server.Implementations.Library
 
             var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval;
 
-            if (!refresh && !item.DisplayParentId.Equals(default))
+            if (!refresh && !item.DisplayParentId.IsEmpty())
             {
                 var displayParent = GetItemById(item.DisplayParentId);
                 refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed;
@@ -2419,7 +2419,7 @@ namespace Emby.Server.Implementations.Library
                 return GetItemById(parentId.Value);
             }
 
-            if (userId.HasValue && !userId.Equals(default))
+            if (!userId.IsNullOrEmpty())
             {
                 return GetUserRootFolder();
             }

+ 10 - 7
Emby.Server.Implementations/Library/LiveStreamHelper.cs

@@ -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 (Exception ex)
+                catch (IOException ex)
                 {
-                    _logger.LogError(ex, "Error deserializing mediainfo cache");
+                    _logger.LogDebug(ex, "Could not open cached media info");
                 }
-                finally
+                catch (Exception ex)
                 {
-                    await jsonStream.DisposeAsync().ConfigureAwait(false);
+                    _logger.LogError(ex, "Error opening cached media info");
                 }
             }
 

+ 3 - 2
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -14,6 +14,7 @@ using System.Threading.Tasks;
 using AsyncKeyedLock;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
@@ -519,10 +520,10 @@ namespace Emby.Server.Implementations.Library
             _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource);
             var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions);
 
-            if (!request.UserId.Equals(default))
+            if (!request.UserId.IsEmpty())
             {
                 var user = _userManager.GetUserById(request.UserId);
-                var item = request.ItemId.Equals(default)
+                var item = request.ItemId.IsEmpty()
                     ? null
                     : _libraryManager.GetItemById(request.ItemId);
                 SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user);

+ 2 - 1
Emby.Server.Implementations/Library/MusicManager.cs

@@ -7,6 +7,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     return Guid.Empty;
                 }
-            }).Where(i => !i.Equals(default)).ToArray();
+            }).Where(i => !i.IsEmpty()).ToArray();
 
             return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
         }

+ 2 - 2
Emby.Server.Implementations/Library/SearchEngine.cs

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library
         public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query)
         {
             User user = null;
-            if (!query.UserId.Equals(default))
+            if (!query.UserId.IsEmpty())
             {
                 user = _userManager.GetUserById(query.UserId);
             }
@@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Library
 
             if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist)
             {
-                if (!searchQuery.ParentId.Equals(default))
+                if (!searchQuery.ParentId.IsEmpty())
                 {
                     searchQuery.AncestorIds = new[] { searchQuery.ParentId };
                     searchQuery.ParentId = Guid.Empty;

+ 3 - 2
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -8,6 +8,7 @@ using System.Linq;
 using System.Threading;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -151,7 +152,7 @@ namespace Emby.Server.Implementations.Library
                     var index = Array.IndexOf(orders, i.Id);
                     if (index == -1
                         && i is UserView view
-                        && !view.DisplayParentId.Equals(default))
+                        && !view.DisplayParentId.IsEmpty())
                     {
                         index = Array.IndexOf(orders, view.DisplayParentId);
                     }
@@ -253,7 +254,7 @@ namespace Emby.Server.Implementations.Library
 
             var parents = new List<BaseItem>();
 
-            if (!parentId.Equals(default))
+            if (!parentId.IsEmpty())
             {
                 var parentItem = _libraryManager.GetItemById(parentId);
                 if (parentItem is Channel)

+ 4 - 2
Emby.Server.Implementations/Localization/Core/et.json

@@ -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."
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/ka.json

@@ -4,9 +4,9 @@
     "HeaderFavoriteAlbums": "რჩეული ალბომები",
     "TasksApplicationCategory": "აპლიკაცია",
     "Albums": "ალბომები",
-    "AppDeviceValues": "აპი: {0}, მოწყობილობა: {1}",
+    "AppDeviceValues": "აპკაცია: {0}, მოწყობილობა: {1}",
     "Application": "აპლიკაცია",
-    "Artists": "შემსრულებლები",
+    "Artists": "არტისტი",
     "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია",
     "Books": "წიგნები",
     "Forced": "ძალით",

+ 1 - 0
Emby.Server.Implementations/Localization/Core/ky.json

@@ -0,0 +1 @@
+{}

+ 3 - 1
Emby.Server.Implementations/Localization/Core/ms.json

@@ -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"
 }

+ 10 - 10
Emby.Server.Implementations/Localization/Core/nb.json

@@ -5,7 +5,7 @@
     "Artists": "Artister",
     "AuthenticationSucceededWithUserName": "{0} har logget inn",
     "Books": "Bøker",
-    "CameraImageUploadedFrom": "Et nytt kamerabilde er lastet opp fra {0}",
+    "CameraImageUploadedFrom": "Et nytt kamerabilde har blitt lastet opp fra {0}",
     "Channels": "Kanaler",
     "ChapterNameValue": "Kapittel {0}",
     "Collections": "Samlinger",
@@ -32,10 +32,10 @@
     "LabelIpAddressValue": "IP-adresse: {0}",
     "LabelRunningTimeValue": "Spilletid {0}",
     "Latest": "Siste",
-    "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert",
-    "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert",
-    "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert",
+    "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert",
+    "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjonsseksjon {0} har blitt oppdatert",
+    "MessageServerConfigurationUpdated": "Serverkonfigurasjon har blitt oppdatert",
     "MixedContent": "Blandet innhold",
     "Movies": "Filmer",
     "Music": "Musikk",
@@ -43,19 +43,19 @@
     "NameInstallFailed": "Installasjonen av {0} mislyktes",
     "NameSeasonNumber": "Sesong {0}",
     "NameSeasonUnknown": "Ukjent sesong",
-    "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.",
+    "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.",
     "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig",
     "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert",
     "NotificationOptionAudioPlayback": "Lydavspilling startet",
     "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet",
     "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp",
-    "NotificationOptionInstallationFailed": "Installasjonen feilet",
+    "NotificationOptionInstallationFailed": "Installasjonsfeil",
     "NotificationOptionNewLibraryContent": "Nytt innhold lagt til",
     "NotificationOptionPluginError": "Programvareutvidelsesfeil",
     "NotificationOptionPluginInstalled": "Programvareutvidelse installert",
     "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert",
     "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert",
-    "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig",
+    "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig",
     "NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave",
     "NotificationOptionUserLockedOut": "Bruker er utestengt",
     "NotificationOptionVideoPlayback": "Videoavspilling startet",
@@ -70,9 +70,9 @@
     "ScheduledTaskFailedWithName": "{0} mislykkes",
     "ScheduledTaskStartedWithName": "{0} startet",
     "ServerNameNeedsToBeRestarted": "{0} må startes på nytt",
-    "Shows": "Program",
+    "Shows": "Serier",
     "Songs": "Sanger",
-    "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.",
+    "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.",
     "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for  {0}",
     "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}",
     "Sync": "Synkroniser",

+ 3 - 1
Emby.Server.Implementations/Localization/Core/sl-SI.json

@@ -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."
 }

+ 2 - 2
Emby.Server.Implementations/Localization/Core/sv.json

@@ -43,7 +43,7 @@
     "NameInstallFailed": "{0} installationen misslyckades",
     "NameSeasonNumber": "Säsong {0}",
     "NameSeasonUnknown": "Okänd säsong",
-    "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.",
+    "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig för nedladdning.",
     "NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig",
     "NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad",
     "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats",
@@ -74,7 +74,7 @@
     "Songs": "Låtar",
     "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.",
     "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades",
-    "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}",
+    "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}",
     "Sync": "Synk",
     "System": "System",
     "TvShows": "TV-serier",

+ 3 - 0
Emby.Server.Implementations/Localization/Core/ur.json

@@ -0,0 +1,3 @@
+{
+    "Books": "کتابیں"
+}

+ 3 - 1
Emby.Server.Implementations/Localization/Core/vi.json

@@ -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."
 }

+ 4 - 4
Emby.Server.Implementations/Localization/Core/zh-HK.json

@@ -83,13 +83,13 @@
     "UserDeletedWithName": "用戶 {0} 已被移除",
     "UserDownloadingItemWithValues": "{0} 正在下載 {1}",
     "UserLockedOutWithName": "用戶 {0} 已被封鎖",
-    "UserOfflineFromDevice": "{0} 從 {1} 斷開連接",
+    "UserOfflineFromDevice": "{0} 終止了 {1} 的連接",
     "UserOnlineFromDevice": "{0} 從 {1} 連線",
-    "UserPasswordChangedWithName": "{0} 的密碼已被改",
+    "UserPasswordChangedWithName": "{0} 的密碼已被改",
     "UserPolicyUpdatedWithName": "使用條款已更新為 {0}",
     "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}",
-    "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}",
-    "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫",
+    "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}",
+    "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫",
     "ValueSpecialEpisodeName": "特典 - {0}",
     "VersionNumber": "版本 {0}",
     "TaskDownloadMissingSubtitles": "下載欠缺字幕",

+ 2 - 1
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -11,6 +11,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Playlists
 
         public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId)
         {
-            var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
 
             return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false)
             {

+ 3 - 2
Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs

@@ -115,9 +115,10 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
         List<LinkedChild>? itemsToRemove = null;
         foreach (var linkedChild in folder.LinkedChildren)
         {
-            if (!File.Exists(folder.Path))
+            var path = linkedChild.Path;
+            if (!File.Exists(path))
             {
-                _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path);
+                _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path);
                 (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
             }
         }

+ 17 - 17
Emby.Server.Implementations/Session/SessionManager.cs

@@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.Session
                 _logger);
         }
 
-        private void OnSessionEnded(SessionInfo info)
+        private async ValueTask OnSessionEnded(SessionInfo info)
         {
             EventHelper.QueueEventIfNotNull(
                 SessionEnded,
@@ -202,7 +202,7 @@ namespace Emby.Server.Implementations.Session
 
             _eventManager.Publish(new SessionEndedEventArgs(info));
 
-            info.Dispose();
+            await info.DisposeAsync().ConfigureAwait(false);
         }
 
         /// <inheritdoc />
@@ -301,12 +301,12 @@ namespace Emby.Server.Implementations.Session
                     await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false);
                 }
 
-                OnSessionEnded(session);
+                await OnSessionEnded(session).ConfigureAwait(false);
             }
         }
 
         /// <inheritdoc />
-        public void ReportSessionEnded(string sessionId)
+        public async ValueTask ReportSessionEnded(string sessionId)
         {
             CheckDisposed();
             var session = GetSession(sessionId, false);
@@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Session
 
                 _activeConnections.TryRemove(key, out _);
 
-                OnSessionEnded(session);
+                await OnSessionEnded(session).ConfigureAwait(false);
             }
         }
 
@@ -337,7 +337,7 @@ namespace Emby.Server.Implementations.Session
                 info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
             }
 
-            if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null)
+            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
             {
                 var current = session.NowPlayingItem;
 
@@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Session
         {
             var users = new List<User>();
 
-            if (session.UserId.Equals(default))
+            if (session.UserId.IsEmpty())
             {
                 return users;
             }
@@ -690,7 +690,7 @@ namespace Emby.Server.Implementations.Session
 
             var session = GetSession(info.SessionId);
 
-            var libraryItem = info.ItemId.Equals(default)
+            var libraryItem = info.ItemId.IsEmpty()
                 ? null
                 : GetNowPlayingItem(session, info.ItemId);
 
@@ -784,7 +784,7 @@ namespace Emby.Server.Implementations.Session
 
             var session = GetSession(info.SessionId);
 
-            var libraryItem = info.ItemId.Equals(default)
+            var libraryItem = info.ItemId.IsEmpty()
                 ? null
                 : GetNowPlayingItem(session, info.ItemId);
 
@@ -923,7 +923,7 @@ namespace Emby.Server.Implementations.Session
 
             session.StopAutomaticProgress();
 
-            var libraryItem = info.ItemId.Equals(default)
+            var libraryItem = info.ItemId.IsEmpty()
                 ? null
                 : GetNowPlayingItem(session, info.ItemId);
 
@@ -933,7 +933,7 @@ namespace Emby.Server.Implementations.Session
                 info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture);
             }
 
-            if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null)
+            if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null)
             {
                 var current = session.NowPlayingItem;
 
@@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Session
 
             var session = GetSessionToRemoteControl(sessionId);
 
-            var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId);
+            var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId);
 
             List<BaseItem> items;
 
@@ -1223,7 +1223,7 @@ namespace Emby.Server.Implementations.Session
             {
                 var controllingSession = GetSession(controllingSessionId);
                 AssertCanControl(session, controllingSession);
-                if (!controllingSession.UserId.Equals(default))
+                if (!controllingSession.UserId.IsEmpty())
                 {
                     command.ControllingUserId = controllingSession.UserId;
                 }
@@ -1342,7 +1342,7 @@ namespace Emby.Server.Implementations.Session
             {
                 var controllingSession = GetSession(controllingSessionId);
                 AssertCanControl(session, controllingSession);
-                if (!controllingSession.UserId.Equals(default))
+                if (!controllingSession.UserId.IsEmpty())
                 {
                     command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture);
                 }
@@ -1463,7 +1463,7 @@ namespace Emby.Server.Implementations.Session
             ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
 
             User user = null;
-            if (!request.UserId.Equals(default))
+            if (!request.UserId.IsEmpty())
             {
                 user = _userManager.GetUserById(request.UserId);
             }
@@ -1590,7 +1590,7 @@ namespace Emby.Server.Implementations.Session
             {
                 try
                 {
-                    ReportSessionEnded(session.Id);
+                    await ReportSessionEnded(session.Id).ConfigureAwait(false);
                 }
                 catch (Exception ex)
                 {
@@ -1766,7 +1766,7 @@ namespace Emby.Server.Implementations.Session
         {
             ArgumentNullException.ThrowIfNull(info);
 
-            var user = info.UserId.Equals(default)
+            var user = info.UserId.IsEmpty()
                 ? null
                 : _userManager.GetUserById(info.UserId);
 

+ 2 - 1
Emby.Server.Implementations/SyncPlay/Group.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.SyncPlay;
@@ -553,7 +554,7 @@ namespace Emby.Server.Implementations.SyncPlay
             if (playingItemRemoved)
             {
                 var itemId = PlayQueue.GetPlayingItemId();
-                if (!itemId.Equals(default))
+                if (!itemId.IsEmpty())
                 {
                     var item = _libraryManager.GetItemById(itemId);
                     RunTimeTicks = item.RunTimeTicks ?? 0;

+ 4 - 3
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Linq;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -41,7 +42,7 @@ namespace Emby.Server.Implementations.TV
             }
 
             string? presentationUniqueKey = null;
-            if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default))
+            if (!query.SeriesId.IsNullOrEmpty())
             {
                 if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series)
                 {
@@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.TV
 
             string? presentationUniqueKey = null;
             int? limit = null;
-            if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default))
+            if (!request.SeriesId.IsNullOrEmpty())
             {
                 if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series)
                 {
@@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.TV
 
             // If viewing all next up for all series, remove first episodes
             // But if that returns empty, keep those first episodes (avoid completely empty view)
-            var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default);
+            var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty();
             var anyFound = false;
 
             return allNextUp

+ 2 - 1
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -11,6 +11,7 @@ using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Data.Events;
+using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
@@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.Updates
                 availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
             }
 
-            if (!id.Equals(default))
+            if (!id.IsEmpty())
             {
                 availablePackages = availablePackages.Where(x => x.Id.Equals(id));
             }

+ 2 - 1
Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs

@@ -2,6 +2,7 @@
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Library;
@@ -41,7 +42,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
             var isApiKey = context.User.GetIsApiKey();
             var userId = context.User.GetUserId();
             // This likely only happens during the wizard, so skip the default checks and let any other handlers do it
-            if (!isApiKey && userId.Equals(default))
+            if (!isApiKey && userId.IsEmpty())
             {
                 return Task.CompletedTask;
             }

+ 2 - 1
Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs

@@ -1,6 +1,7 @@
 using System.Threading.Tasks;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Library;
@@ -46,7 +47,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
             }
 
             var userId = contextUser.GetUserId();
-            if (userId.Equals(default))
+            if (userId.IsEmpty())
             {
                 context.Fail();
                 return Task.CompletedTask;

+ 4 - 3
Jellyfin.Api/Controllers/ArtistsController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -126,7 +127,7 @@ public class ArtistsController : BaseJellyfinApiController
         User? user = null;
         BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             user = _userManager.GetUserById(userId.Value);
         }
@@ -330,7 +331,7 @@ public class ArtistsController : BaseJellyfinApiController
         User? user = null;
         BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             user = _userManager.GetUserById(userId.Value);
         }
@@ -469,7 +470,7 @@ public class ArtistsController : BaseJellyfinApiController
 
         var item = _libraryManager.GetArtist(name, dtoOptions);
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             var user = _userManager.GetUserById(userId.Value);
 

+ 3 - 2
Jellyfin.Api/Controllers/ChannelsController.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -126,7 +127,7 @@ public class ChannelsController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -201,7 +202,7 @@ public class ChannelsController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 

+ 3 - 2
Jellyfin.Api/Controllers/FilterController.cs

@@ -3,6 +3,7 @@ using System.Linq;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -53,7 +54,7 @@ public class FilterController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -146,7 +147,7 @@ public class FilterController : BaseJellyfinApiController
         [FromQuery] bool? recursive)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 

+ 3 - 2
Jellyfin.Api/Controllers/GenresController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -95,7 +96,7 @@ public class GenresController : BaseJellyfinApiController
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 
-        User? user = userId.Value.Equals(default)
+        User? user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -172,7 +173,7 @@ public class GenresController : BaseJellyfinApiController
 
         item ??= new Genre();
 
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 

+ 8 - 7
Jellyfin.Api/Controllers/InstantMixController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -76,7 +77,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var item = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -113,7 +114,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var album = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -150,7 +151,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var playlist = (Playlist)_libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -186,7 +187,7 @@ public class InstantMixController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -223,7 +224,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var item = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -260,7 +261,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var item = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }
@@ -334,7 +335,7 @@ public class InstantMixController : BaseJellyfinApiController
     {
         var item = _libraryManager.GetItemById(id);
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }

+ 3 - 2
Jellyfin.Api/Controllers/ItemsController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -245,7 +246,7 @@ public class ItemsController : BaseJellyfinApiController
         var isApiKey = User.GetIsApiKey();
         // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = !isApiKey && !userId.Value.Equals(default)
+        var user = !isApiKey && !userId.IsNullOrEmpty()
             ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException()
             : null;
 
@@ -840,7 +841,7 @@ public class ItemsController : BaseJellyfinApiController
         var ancestorIds = Array.Empty<Guid>();
 
         var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes);
-        if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0)
+        if (parentIdGuid.IsEmpty() && excludeFolderIds.Length > 0)
         {
             ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
                 .Where(i => i is Folder)

+ 13 - 13
Jellyfin.Api/Controllers/LibraryController.cs

@@ -146,12 +146,12 @@ public class LibraryController : BaseJellyfinApiController
         [FromQuery] bool inheritFromParent = false)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
-        var item = itemId.Equals(default)
-            ? (userId.Value.Equals(default)
+        var item = itemId.IsEmpty()
+            ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
             : _libraryManager.GetItemById(itemId);
@@ -213,12 +213,12 @@ public class LibraryController : BaseJellyfinApiController
         [FromQuery] bool inheritFromParent = false)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
-        var item = itemId.Equals(default)
-            ? (userId.Value.Equals(default)
+        var item = itemId.IsEmpty()
+            ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
             : _libraryManager.GetItemById(itemId);
@@ -339,7 +339,7 @@ public class LibraryController : BaseJellyfinApiController
     {
         var isApiKey = User.GetIsApiKey();
         var userId = User.GetUserId();
-        var user = !isApiKey && !userId.Equals(default)
+        var user = !isApiKey && !userId.IsEmpty()
             ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
             : null;
         if (!isApiKey && user is null)
@@ -382,7 +382,7 @@ public class LibraryController : BaseJellyfinApiController
     {
         var isApiKey = User.GetIsApiKey();
         var userId = User.GetUserId();
-        var user = !isApiKey && !userId.Equals(default)
+        var user = !isApiKey && !userId.IsEmpty()
             ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException()
             : null;
 
@@ -428,7 +428,7 @@ public class LibraryController : BaseJellyfinApiController
         [FromQuery] bool? isFavorite)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -471,7 +471,7 @@ public class LibraryController : BaseJellyfinApiController
 
         var baseItemDtos = new List<BaseItemDto>();
 
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -702,8 +702,8 @@ public class LibraryController : BaseJellyfinApiController
         [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var item = itemId.Equals(default)
-            ? (userId.Value.Equals(default)
+        var item = itemId.IsEmpty()
+            ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
             : _libraryManager.GetItemById(itemId);
@@ -718,7 +718,7 @@ public class LibraryController : BaseJellyfinApiController
             return new QueryResult<BaseItemDto>();
         }
 
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }

+ 27 - 27
Jellyfin.Api/Controllers/LiveTvController.cs

@@ -10,12 +10,12 @@ 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.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.LiveTvDtos;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
@@ -43,6 +43,8 @@ namespace Jellyfin.Api.Controllers;
 public class LiveTvController : BaseJellyfinApiController
 {
     private readonly ILiveTvManager _liveTvManager;
+    private readonly IGuideManager _guideManager;
+    private readonly ITunerHostManager _tunerHostManager;
     private readonly IUserManager _userManager;
     private readonly IHttpClientFactory _httpClientFactory;
     private readonly ILibraryManager _libraryManager;
@@ -55,6 +57,8 @@ 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="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>
@@ -64,6 +68,8 @@ public class LiveTvController : BaseJellyfinApiController
     /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param>
     public LiveTvController(
         ILiveTvManager liveTvManager,
+        IGuideManager guideManager,
+        ITunerHostManager tunerHostManager,
         IUserManager userManager,
         IHttpClientFactory httpClientFactory,
         ILibraryManager libraryManager,
@@ -73,6 +79,8 @@ public class LiveTvController : BaseJellyfinApiController
         ITranscodeManager transcodeManager)
     {
         _liveTvManager = liveTvManager;
+        _guideManager = guideManager;
+        _tunerHostManager = tunerHostManager;
         _userManager = userManager;
         _httpClientFactory = httpClientFactory;
         _libraryManager = libraryManager;
@@ -179,7 +187,7 @@ public class LiveTvController : BaseJellyfinApiController
             dtoOptions,
             CancellationToken.None);
 
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -211,10 +219,10 @@ public class LiveTvController : BaseJellyfinApiController
     public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
-        var item = channelId.Equals(default)
+        var item = channelId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(channelId);
 
@@ -384,7 +392,7 @@ public class LiveTvController : BaseJellyfinApiController
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false);
@@ -407,10 +415,10 @@ public class LiveTvController : BaseJellyfinApiController
     public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
-        var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
+        var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId);
 
         var dtoOptions = new DtoOptions()
             .AddClientFields(User);
@@ -564,7 +572,7 @@ public class LiveTvController : BaseJellyfinApiController
         [FromQuery] bool enableTotalRecordCount = true)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -591,7 +599,7 @@ public class LiveTvController : BaseJellyfinApiController
             GenreIds = genreIds
         };
 
-        if (librarySeriesId.HasValue && !librarySeriesId.Equals(default))
+        if (!librarySeriesId.IsNullOrEmpty())
         {
             query.IsSeries = true;
 
@@ -620,7 +628,7 @@ public class LiveTvController : BaseJellyfinApiController
     [Authorize(Policy = Policies.LiveTvAccess)]
     public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body)
     {
-        var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId);
+        var user = body.UserId.IsEmpty() ? null : _userManager.GetUserById(body.UserId);
 
         var query = new InternalItemsQuery(user)
         {
@@ -645,7 +653,7 @@ public class LiveTvController : BaseJellyfinApiController
             GenreIds = body.GenreIds
         };
 
-        if (!body.LibrarySeriesId.Equals(default))
+        if (!body.LibrarySeriesId.IsEmpty())
         {
             query.IsSeries = true;
 
@@ -704,7 +712,7 @@ public class LiveTvController : BaseJellyfinApiController
         [FromQuery] bool enableTotalRecordCount = true)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -743,7 +751,7 @@ public class LiveTvController : BaseJellyfinApiController
         [FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -937,9 +945,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.
@@ -951,9 +957,7 @@ public class LiveTvController : BaseJellyfinApiController
     [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo)
-    {
-        return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
-    }
+        => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false);
 
     /// <summary>
     /// Deletes a tuner host.
@@ -1130,10 +1134,8 @@ public class LiveTvController : BaseJellyfinApiController
     [HttpGet("TunerHosts/Types")]
     [Authorize(Policy = Policies.LiveTvAccess)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes()
-    {
-        return _liveTvManager.GetTunerHostTypes();
-    }
+    public IEnumerable<NameIdPair> GetTunerHostTypes()
+        => _tunerHostManager.GetTunerHostTypes();
 
     /// <summary>
     /// Discover tuners.
@@ -1145,10 +1147,8 @@ public class LiveTvController : BaseJellyfinApiController
     [HttpGet("Tuners/Discover")]
     [Authorize(Policy = Policies.LiveTvManagement)]
     [ProducesResponseType(StatusCodes.Status200OK)]
-    public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
-    {
-        return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false);
-    }
+    public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false)
+        => _tunerHostManager.DiscoverTuners(newDevicesOnly);
 
     /// <summary>
     /// Gets a live tv recording stream.

+ 2 - 1
Jellyfin.Api/Controllers/MoviesController.cs

@@ -7,6 +7,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -69,7 +70,7 @@ public class MoviesController : BaseJellyfinApiController
         [FromQuery] int itemLimit = 8)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var dtoOptions = new DtoOptions { Fields = fields }

+ 3 - 2
Jellyfin.Api/Controllers/MusicGenresController.cs

@@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -95,7 +96,7 @@ public class MusicGenresController : BaseJellyfinApiController
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes);
 
-        User? user = userId.Value.Equals(default)
+        User? user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -164,7 +165,7 @@ public class MusicGenresController : BaseJellyfinApiController
             return NotFound();
         }
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             var user = _userManager.GetUserById(userId.Value);
 

+ 3 - 2
Jellyfin.Api/Controllers/PersonsController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -83,7 +84,7 @@ public class PersonsController : BaseJellyfinApiController
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
-        User? user = userId.Value.Equals(default)
+        User? user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -129,7 +130,7 @@ public class PersonsController : BaseJellyfinApiController
             return NotFound();
         }
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             var user = _userManager.GetUserById(userId.Value);
             return _dtoService.GetBaseItemDto(item, dtoOptions, user);

+ 2 - 1
Jellyfin.Api/Controllers/PlaylistsController.cs

@@ -9,6 +9,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.PlaylistDtos;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Playlists;
@@ -188,7 +189,7 @@ public class PlaylistsController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var user = userId.Equals(default)
+        var user = userId.IsEmpty()
             ? null
             : _userManager.GetUserById(userId);
 

+ 1 - 1
Jellyfin.Api/Controllers/SearchController.cs

@@ -209,7 +209,7 @@ public class SearchController : BaseJellyfinApiController
                 break;
         }
 
-        if (!item.ChannelId.Equals(default))
+        if (!item.ChannelId.IsEmpty())
         {
             var channel = _libraryManager.GetItemById(item.ChannelId);
             result.ChannelName = channel?.Name;

+ 4 - 3
Jellyfin.Api/Controllers/SessionController.cs

@@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Api.Models.SessionDtos;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Library;
@@ -71,7 +72,7 @@ public class SessionController : BaseJellyfinApiController
             result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase));
         }
 
-        if (controllableByUserId.HasValue && !controllableByUserId.Equals(default))
+        if (!controllableByUserId.IsNullOrEmpty())
         {
             result = result.Where(i => i.SupportsRemoteControl);
 
@@ -83,12 +84,12 @@ public class SessionController : BaseJellyfinApiController
 
             if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
             {
-                result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value));
+                result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableByUserId.Value));
             }
 
             if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
             {
-                result = result.Where(i => !i.UserId.Equals(default));
+                result = result.Where(i => !i.UserId.IsEmpty());
             }
 
             result = result.Where(i =>

+ 3 - 2
Jellyfin.Api/Controllers/StudiosController.cs

@@ -5,6 +5,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -91,7 +92,7 @@ public class StudiosController : BaseJellyfinApiController
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
-        User? user = userId.Value.Equals(default)
+        User? user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -144,7 +145,7 @@ public class StudiosController : BaseJellyfinApiController
         var dtoOptions = new DtoOptions().AddClientFields(User);
 
         var item = _libraryManager.GetStudio(name);
-        if (!userId.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             var user = _userManager.GetUserById(userId.Value);
 

+ 2 - 1
Jellyfin.Api/Controllers/SuggestionsController.cs

@@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ public class SuggestionsController : BaseJellyfinApiController
         [FromQuery] int? limit,
         [FromQuery] bool enableTotalRecordCount = false)
     {
-        var user = userId.Equals(default)
+        var user = userId.IsEmpty()
             ? null
             : _userManager.GetUserById(userId);
 

+ 5 - 5
Jellyfin.Api/Controllers/TvShowsController.cs

@@ -111,7 +111,7 @@ public class TvShowsController : BaseJellyfinApiController
             },
             options);
 
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -150,7 +150,7 @@ public class TvShowsController : BaseJellyfinApiController
         [FromQuery] bool? enableUserData)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -222,7 +222,7 @@ public class TvShowsController : BaseJellyfinApiController
         [FromQuery] ItemSortBy? sortBy)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
@@ -284,7 +284,7 @@ public class TvShowsController : BaseJellyfinApiController
         }
 
         // This must be the last filter
-        if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default))
+        if (!adjacentTo.IsNullOrEmpty())
         {
             episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList();
         }
@@ -339,7 +339,7 @@ public class TvShowsController : BaseJellyfinApiController
         [FromQuery] bool? enableUserData)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 

+ 2 - 1
Jellyfin.Api/Controllers/UserController.cs

@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.Models.UserDtos;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -532,7 +533,7 @@ public class UserController : BaseJellyfinApiController
     public ActionResult<UserDto> GetCurrentUser()
     {
         var userId = User.GetUserId();
-        if (userId.Equals(default))
+        if (userId.IsEmpty())
         {
             return BadRequest();
         }

+ 10 - 9
Jellyfin.Api/Controllers/UserLibraryController.cs

@@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions;
 using Jellyfin.Api.ModelBinders;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
@@ -84,7 +85,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -145,7 +146,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -185,7 +186,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -221,7 +222,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -257,7 +258,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -294,7 +295,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -330,7 +331,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -375,7 +376,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 
@@ -558,7 +559,7 @@ public class UserLibraryController : BaseJellyfinApiController
             return NotFound();
         }
 
-        var item = itemId.Equals(default)
+        var item = itemId.IsEmpty()
             ? _libraryManager.GetUserRootFolder()
             : _libraryManager.GetItemById(itemId);
 

+ 4 - 3
Jellyfin.Api/Controllers/VideosController.cs

@@ -11,6 +11,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Helpers;
 using Jellyfin.Api.ModelBinders;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Api;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Net;
@@ -96,12 +97,12 @@ public class VideosController : BaseJellyfinApiController
     public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId)
     {
         userId = RequestHelpers.GetUserId(User, userId);
-        var user = userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
 
-        var item = itemId.Equals(default)
-            ? (userId.Value.Equals(default)
+        var item = itemId.IsEmpty()
+            ? (userId.IsNullOrEmpty()
                 ? _libraryManager.RootFolder
                 : _libraryManager.GetUserRootFolder())
             : _libraryManager.GetItemById(itemId);

+ 3 - 3
Jellyfin.Api/Controllers/YearsController.cs

@@ -90,7 +90,7 @@ public class YearsController : BaseJellyfinApiController
             .AddClientFields(User)
             .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);
 
-        User? user = userId.Value.Equals(default)
+        User? user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId);
@@ -110,7 +110,7 @@ public class YearsController : BaseJellyfinApiController
         {
             var folder = (Folder)parentItem;
 
-            if (userId.Equals(default))
+            if (userId.IsNullOrEmpty())
             {
                 items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList();
             }
@@ -182,7 +182,7 @@ public class YearsController : BaseJellyfinApiController
         var dtoOptions = new DtoOptions()
             .AddClientFields(User);
 
-        if (!userId.Value.Equals(default))
+        if (!userId.IsNullOrEmpty())
         {
             var user = _userManager.GetUserById(userId.Value);
             return _dtoService.GetBaseItemDto(item, dtoOptions, user);

+ 2 - 1
Jellyfin.Api/Helpers/MediaInfoHelper.cs

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Configuration;
@@ -86,7 +87,7 @@ public class MediaInfoHelper
         string? mediaSourceId = null,
         string? liveStreamId = null)
     {
-        var user = userId is null || userId.Value.Equals(default)
+        var user = userId.IsNullOrEmpty()
             ? null
             : _userManager.GetUserById(userId.Value);
         var item = _libraryManager.GetItemById(id);

+ 2 - 1
Jellyfin.Api/Helpers/RequestHelpers.cs

@@ -7,6 +7,7 @@ using Jellyfin.Api.Constants;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -67,7 +68,7 @@ public static class RequestHelpers
         var authenticatedUserId = claimsPrincipal.GetUserId();
 
         // UserId not provided, fall back to authenticated user id.
-        if (userId is null || userId.Value.Equals(default))
+        if (userId.IsNullOrEmpty())
         {
             return authenticatedUserId;
         }

+ 1 - 1
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -82,7 +82,7 @@ public static class StreamingHelpers
         };
 
         var userId = httpContext.User.GetUserId();
-        if (!userId.Equals(default))
+        if (!userId.IsEmpty())
         {
             state.User = userManager.GetUserById(userId);
         }

+ 2 - 1
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -11,6 +11,7 @@ using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events.Users;
+using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -117,7 +118,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public User? GetUserById(Guid id)
         {
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }

+ 0 - 9
Jellyfin.Server/CoreAppHost.cs

@@ -7,7 +7,6 @@ using Jellyfin.Api.WebSocketListeners;
 using Jellyfin.Drawing;
 using Jellyfin.Drawing.Skia;
 using Jellyfin.LiveTv;
-using Jellyfin.LiveTv.Channels;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Devices;
@@ -18,18 +17,15 @@ using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.BaseItemManager;
-using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model.Activity;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Providers.Lyric;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
@@ -101,11 +97,6 @@ namespace Jellyfin.Server
 
             serviceCollection.AddScoped<IAuthenticationManager, AuthenticationManager>();
 
-            serviceCollection.AddSingleton<LiveTvDtoService>();
-            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
-            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
-            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
-
             foreach (var type in GetExportTypes<ILyricProvider>())
             {
                 serviceCollection.AddSingleton(typeof(ILyricProvider), type);

+ 2 - 0
Jellyfin.Server/Startup.cs

@@ -5,6 +5,7 @@ using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Text;
 using Jellyfin.Api.Middleware;
+using Jellyfin.LiveTv.Extensions;
 using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.Networking;
 using Jellyfin.Networking.HappyEyeballs;
@@ -121,6 +122,7 @@ namespace Jellyfin.Server
                 .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
 
             services.AddHlsPlaylistGenerator();
+            services.AddLiveTvServices();
 
             services.AddHostedService<AutoDiscoveryHost>();
         }

+ 0 - 7
MediaBrowser.Controller/Channels/IChannelManager.cs

@@ -95,12 +95,5 @@ namespace MediaBrowser.Controller.Channels
         /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns>The item media sources.</returns>
         IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
-
-        /// <summary>
-        /// Whether the item supports media probe.
-        /// </summary>
-        /// <param name="item">The item.</param>
-        /// <returns>Whether media probe should be enabled.</returns>
-        bool EnableMediaProbe(BaseItem item);
     }
 }

+ 2 - 1
MediaBrowser.Controller/Entities/AggregateFolder.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text.Json.Serialization;
 using System.Threading;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -184,7 +185,7 @@ namespace MediaBrowser.Controller.Entities
         /// <exception cref="ArgumentNullException">The id is empty.</exception>
         public BaseItem FindVirtualChild(Guid id)
         {
-            if (id.Equals(default))
+            if (id.IsEmpty())
             {
                 throw new ArgumentNullException(nameof(id));
             }

+ 1 - 1
MediaBrowser.Controller/Entities/Audio/MusicArtist.cs

@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.Audio
     public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
     {
         [JsonIgnore]
-        public bool IsAccessedByName => ParentId.Equals(default);
+        public bool IsAccessedByName => ParentId.IsEmpty();
 
         [JsonIgnore]
         public override bool IsFolder => !IsAccessedByName;

+ 8 - 8
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -240,7 +240,7 @@ namespace MediaBrowser.Controller.Entities
         {
             get
             {
-                if (!ChannelId.Equals(default))
+                if (!ChannelId.IsEmpty())
                 {
                     return SourceType.Channel;
                 }
@@ -530,7 +530,7 @@ namespace MediaBrowser.Controller.Entities
             get
             {
                 var id = DisplayParentId;
-                if (id.Equals(default))
+                if (id.IsEmpty())
                 {
                     return null;
                 }
@@ -746,7 +746,7 @@ namespace MediaBrowser.Controller.Entities
         public virtual bool StopRefreshIfLocalMetadataFound => true;
 
         [JsonIgnore]
-        protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol;
+        protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol;
 
         [JsonIgnore]
         public virtual bool SupportsPeople => false;
@@ -823,7 +823,7 @@ namespace MediaBrowser.Controller.Entities
         public BaseItem GetOwner()
         {
             var ownerId = OwnerId;
-            return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId);
+            return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId);
         }
 
         public bool CanDelete(User user, List<Folder> allCollectionFolders)
@@ -968,7 +968,7 @@ namespace MediaBrowser.Controller.Entities
         public BaseItem GetParent()
         {
             var parentId = ParentId;
-            if (parentId.Equals(default))
+            if (parentId.IsEmpty())
             {
                 return null;
             }
@@ -1361,7 +1361,7 @@ namespace MediaBrowser.Controller.Entities
             var tasks = extras.Select(i =>
             {
                 var subOptions = new MetadataRefreshOptions(options);
-                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default))
+                if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty())
                 {
                     i.OwnerId = ownerId;
                     i.ParentId = Guid.Empty;
@@ -1673,7 +1673,7 @@ namespace MediaBrowser.Controller.Entities
             // First get using the cached Id
             if (info.ItemId.HasValue)
             {
-                if (info.ItemId.Value.Equals(default))
+                if (info.ItemId.Value.IsEmpty())
                 {
                     return null;
                 }
@@ -2439,7 +2439,7 @@ namespace MediaBrowser.Controller.Entities
                 return Task.FromResult(true);
             }
 
-            if (video.OwnerId.Equals(default))
+            if (video.OwnerId.IsEmpty())
             {
                 video.OwnerId = this.Id;
             }

+ 5 - 4
MediaBrowser.Controller/Entities/Folder.cs

@@ -12,6 +12,7 @@ using System.Threading.Tasks;
 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;
@@ -198,7 +199,7 @@ namespace MediaBrowser.Controller.Entities
         {
             item.SetParent(this);
 
-            if (item.Id.Equals(default))
+            if (item.Id.IsEmpty())
             {
                 item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType());
             }
@@ -697,7 +698,7 @@ namespace MediaBrowser.Controller.Entities
 
             if (this is not UserRootFolder
                 && this is not AggregateFolder
-                && query.ParentId.Equals(default))
+                && query.ParentId.IsEmpty())
             {
                 query.Parent = this;
             }
@@ -840,7 +841,7 @@ namespace MediaBrowser.Controller.Entities
                 return true;
             }
 
-            if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+            if (!query.AdjacentTo.IsNullOrEmpty())
             {
                 Logger.LogDebug("Query requires post-filtering due to AdjacentTo");
                 return true;
@@ -987,7 +988,7 @@ namespace MediaBrowser.Controller.Entities
             #pragma warning restore CA1309
 
             // This must be the last filter
-            if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+            if (!query.AdjacentTo.IsNullOrEmpty())
             {
                 items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
             }

+ 6 - 5
MediaBrowser.Controller/Entities/TV/Episode.cs

@@ -8,6 +8,7 @@ using System.Globalization;
 using System.Linq;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -74,12 +75,12 @@ namespace MediaBrowser.Controller.Entities.TV
             get
             {
                 var seriesId = SeriesId;
-                if (seriesId.Equals(default))
+                if (seriesId.IsEmpty())
                 {
                     seriesId = FindSeriesId();
                 }
 
-                return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series);
+                return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
             }
         }
 
@@ -89,12 +90,12 @@ namespace MediaBrowser.Controller.Entities.TV
             get
             {
                 var seasonId = SeasonId;
-                if (seasonId.Equals(default))
+                if (seasonId.IsEmpty())
                 {
                     seasonId = FindSeasonId();
                 }
 
-                return seasonId.Equals(default) ? null : (LibraryManager.GetItemById(seasonId) as Season);
+                return seasonId.IsEmpty() ? null : (LibraryManager.GetItemById(seasonId) as Season);
             }
         }
 
@@ -271,7 +272,7 @@ namespace MediaBrowser.Controller.Entities.TV
 
             var seasonId = SeasonId;
 
-            if (!seasonId.Equals(default) && !list.Contains(seasonId))
+            if (!seasonId.IsEmpty() && !list.Contains(seasonId))
             {
                 list.Add(seasonId);
             }

+ 3 - 2
MediaBrowser.Controller/Entities/TV/Season.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text.Json.Serialization;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Querying;
@@ -48,12 +49,12 @@ namespace MediaBrowser.Controller.Entities.TV
             get
             {
                 var seriesId = SeriesId;
-                if (seriesId.Equals(default))
+                if (seriesId.IsEmpty())
                 {
                     seriesId = FindSeriesId();
                 }
 
-                return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series);
+                return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
             }
         }
 

+ 4 - 4
MediaBrowser.Controller/Entities/UserView.cs

@@ -70,11 +70,11 @@ namespace MediaBrowser.Controller.Entities
         /// <inheritdoc />
         public override IEnumerable<Guid> GetIdsForAncestorQuery()
         {
-            if (!DisplayParentId.Equals(default))
+            if (!DisplayParentId.IsEmpty())
             {
                 yield return DisplayParentId;
             }
-            else if (!ParentId.Equals(default))
+            else if (!ParentId.IsEmpty())
             {
                 yield return ParentId;
             }
@@ -95,11 +95,11 @@ namespace MediaBrowser.Controller.Entities
         {
             var parent = this as Folder;
 
-            if (!DisplayParentId.Equals(default))
+            if (!DisplayParentId.IsEmpty())
             {
                 parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent;
             }
-            else if (!ParentId.Equals(default))
+            else if (!ParentId.IsEmpty())
             {
                 parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
             }

+ 1 - 1
MediaBrowser.Controller/Entities/UserViewBuilder.cs

@@ -433,7 +433,7 @@ namespace MediaBrowser.Controller.Entities
             var user = query.User;
 
             // This must be the last filter
-            if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default))
+            if (!query.AdjacentTo.IsNullOrEmpty())
             {
                 items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value);
             }

+ 1 - 1
MediaBrowser.Controller/Entities/Video.cs

@@ -456,7 +456,7 @@ namespace MediaBrowser.Controller.Entities
             foreach (var child in LinkedAlternateVersions)
             {
                 // Reset the cached value
-                if (child.ItemId.HasValue && child.ItemId.Value.Equals(default))
+                if (child.ItemId.IsNullOrEmpty())
                 {
                     child.ItemId = null;
                 }

+ 26 - 0
MediaBrowser.Controller/LiveTv/IGuideManager.cs

@@ -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);
+}

+ 1 - 20
MediaBrowser.Controller/LiveTv/ILiveTvManager.cs

@@ -71,9 +71,8 @@ namespace MediaBrowser.Controller.LiveTv
         /// Adds the parts.
         /// </summary>
         /// <param name="services">The services.</param>
-        /// <param name="tunerHosts">The tuner hosts.</param>
         /// <param name="listingProviders">The listing providers.</param>
-        void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders);
+        void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders);
 
         /// <summary>
         /// Gets the timer.
@@ -175,12 +174,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>
@@ -253,14 +246,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 tuner host.
-        /// </summary>
-        /// <param name="info">Turner host to save.</param>
-        /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
-        /// <returns>Tuner host information wrapped in a task.</returns>
-        Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
-
         /// <summary>
         /// Saves the listing provider.
         /// </summary>
@@ -298,10 +283,6 @@ namespace MediaBrowser.Controller.LiveTv
 
         Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken);
 
-        List<NameIdPair> GetTunerHostTypes();
-
-        Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken);
-
         string GetEmbyTvActiveRecordingPath(string id);
 
         ActiveRecordingInfo GetActiveRecordingInfo(string path);

+ 0 - 13
MediaBrowser.Controller/LiveTv/ILiveTvService.cs

@@ -140,14 +140,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task.</returns>
         Task CloseLiveStream(string id, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Records the live stream.
-        /// </summary>
-        /// <param name="id">The identifier.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        Task RecordLiveStream(string id, CancellationToken cancellationToken);
-
         /// <summary>
         /// Resets the tuner.
         /// </summary>
@@ -180,9 +172,4 @@ namespace MediaBrowser.Controller.LiveTv
     {
         Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken);
     }
-
-    public interface ISupportsUpdatingDefaults
-    {
-        Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken);
-    }
 }

+ 0 - 7
MediaBrowser.Controller/LiveTv/ITunerHost.cs

@@ -35,13 +35,6 @@ namespace MediaBrowser.Controller.LiveTv
         /// <returns>Task&lt;IEnumerable&lt;ChannelInfo&gt;&gt;.</returns>
         Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken);
 
-        /// <summary>
-        /// Gets the tuner infos.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task&lt;List&lt;LiveTvTunerInfo&gt;&gt;.</returns>
-        Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken);
-
         /// <summary>
         /// Gets the channel stream.
         /// </summary>

+ 46 - 0
MediaBrowser.Controller/LiveTv/ITunerHostManager.cs

@@ -0,0 +1,46 @@
+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 the <see cref="ITunerHost"/>s.
+/// </summary>
+public interface ITunerHostManager
+{
+    /// <summary>
+    /// Gets the available <see cref="ITunerHost"/>s.
+    /// </summary>
+    IReadOnlyList<ITunerHost> TunerHosts { get; }
+
+    /// <summary>
+    /// Gets the <see cref="NameIdPair"/>s for the available <see cref="ITunerHost"/>s.
+    /// </summary>
+    /// <returns>The <see cref="NameIdPair"/>s.</returns>
+    IEnumerable<NameIdPair> GetTunerHostTypes();
+
+    /// <summary>
+    /// Saves the tuner host.
+    /// </summary>
+    /// <param name="info">Turner host to save.</param>
+    /// <param name="dataSourceChanged">Option to specify that data source has changed.</param>
+    /// <returns>Tuner host information wrapped in a task.</returns>
+    Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true);
+
+    /// <summary>
+    /// Discovers the available tuners.
+    /// </summary>
+    /// <param name="newDevicesOnly">A value indicating whether to only return new devices.</param>
+    /// <returns>The <see cref="TunerHostInfo"/>s.</returns>
+    IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly);
+
+    /// <summary>
+    /// Scans for tuner devices that have changed URLs.
+    /// </summary>
+    /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param>
+    /// <returns>A task that represents the scanning operation.</returns>
+    Task ScanForTunerDeviceChanges(CancellationToken cancellationToken);
+}

+ 0 - 54
MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs

@@ -1,54 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class LiveTvServiceStatusInfo
-    {
-        public LiveTvServiceStatusInfo()
-        {
-            Tuners = new List<LiveTvTunerInfo>();
-            IsVisible = true;
-        }
-
-        /// <summary>
-        /// Gets or sets the status.
-        /// </summary>
-        /// <value>The status.</value>
-        public LiveTvServiceStatus Status { get; set; }
-
-        /// <summary>
-        /// Gets or sets the status message.
-        /// </summary>
-        /// <value>The status message.</value>
-        public string StatusMessage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the version.
-        /// </summary>
-        /// <value>The version.</value>
-        public string Version { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance has update available.
-        /// </summary>
-        /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value>
-        public bool HasUpdateAvailable { get; set; }
-
-        /// <summary>
-        /// Gets or sets the tuners.
-        /// </summary>
-        /// <value>The tuners.</value>
-        public List<LiveTvTunerInfo> Tuners { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is visible.
-        /// </summary>
-        /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value>
-        public bool IsVisible { get; set; }
-    }
-}

+ 0 - 77
MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs

@@ -1,77 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class LiveTvTunerInfo
-    {
-        public LiveTvTunerInfo()
-        {
-            Clients = new List<string>();
-        }
-
-        /// <summary>
-        /// Gets or sets the type of the source.
-        /// </summary>
-        /// <value>The type of the source.</value>
-        public string SourceType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name.
-        /// </summary>
-        /// <value>The name.</value>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the identifier.
-        /// </summary>
-        /// <value>The identifier.</value>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
-        /// <summary>
-        /// Gets or sets the status.
-        /// </summary>
-        /// <value>The status.</value>
-        public LiveTvTunerStatus Status { get; set; }
-
-        /// <summary>
-        /// Gets or sets the channel identifier.
-        /// </summary>
-        /// <value>The channel identifier.</value>
-        public string ChannelId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the recording identifier.
-        /// </summary>
-        /// <value>The recording identifier.</value>
-        public string RecordingId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the program.
-        /// </summary>
-        /// <value>The name of the program.</value>
-        public string ProgramName { get; set; }
-
-        /// <summary>
-        /// Gets or sets the clients.
-        /// </summary>
-        /// <value>The clients.</value>
-        public List<string> Clients { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance can reset.
-        /// </summary>
-        /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value>
-        public bool CanReset { get; set; }
-    }
-}

+ 0 - 210
MediaBrowser.Controller/LiveTv/RecordingInfo.cs

@@ -1,210 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class RecordingInfo
-    {
-        public RecordingInfo()
-        {
-            Genres = new List<string>();
-        }
-
-        /// <summary>
-        /// Gets or sets the id of the recording.
-        /// </summary>
-        public string Id { get; set; }
-
-        /// <summary>
-        /// Gets or sets the series timer identifier.
-        /// </summary>
-        /// <value>The series timer identifier.</value>
-        public string SeriesTimerId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the timer identifier.
-        /// </summary>
-        /// <value>The timer identifier.</value>
-        public string TimerId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the channelId of the recording.
-        /// </summary>
-        public string ChannelId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the type of the channel.
-        /// </summary>
-        /// <value>The type of the channel.</value>
-        public ChannelType ChannelType { get; set; }
-
-        /// <summary>
-        /// Gets or sets the name of the recording.
-        /// </summary>
-        public string Name { get; set; }
-
-        /// <summary>
-        /// Gets or sets the path.
-        /// </summary>
-        /// <value>The path.</value>
-        public string Path { get; set; }
-
-        /// <summary>
-        /// Gets or sets the URL.
-        /// </summary>
-        /// <value>The URL.</value>
-        public string Url { get; set; }
-
-        /// <summary>
-        /// Gets or sets the overview.
-        /// </summary>
-        /// <value>The overview.</value>
-        public string Overview { get; set; }
-
-        /// <summary>
-        /// Gets or sets the start date of the recording, in UTC.
-        /// </summary>
-        public DateTime StartDate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the end date of the recording, in UTC.
-        /// </summary>
-        public DateTime EndDate { get; set; }
-
-        /// <summary>
-        /// Gets or sets the program identifier.
-        /// </summary>
-        /// <value>The program identifier.</value>
-        public string ProgramId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the status.
-        /// </summary>
-        /// <value>The status.</value>
-        public RecordingStatus Status { get; set; }
-
-        /// <summary>
-        /// Gets or sets the genre of the program.
-        /// </summary>
-        public List<string> Genres { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is repeat.
-        /// </summary>
-        /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value>
-        public bool IsRepeat { get; set; }
-
-        /// <summary>
-        /// Gets or sets the episode title.
-        /// </summary>
-        /// <value>The episode title.</value>
-        public string EpisodeTitle { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is hd.
-        /// </summary>
-        /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value>
-        public bool? IsHD { get; set; }
-
-        /// <summary>
-        /// Gets or sets the audio.
-        /// </summary>
-        /// <value>The audio.</value>
-        public ProgramAudio? Audio { get; set; }
-
-        /// <summary>
-        /// Gets or sets the original air date.
-        /// </summary>
-        /// <value>The original air date.</value>
-        public DateTime? OriginalAirDate { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is movie.
-        /// </summary>
-        /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value>
-        public bool IsMovie { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is sports.
-        /// </summary>
-        /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value>
-        public bool IsSports { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is series.
-        /// </summary>
-        /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value>
-        public bool IsSeries { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is live.
-        /// </summary>
-        /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value>
-        public bool IsLive { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is news.
-        /// </summary>
-        /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value>
-        public bool IsNews { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is kids.
-        /// </summary>
-        /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value>
-        public bool IsKids { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance is premiere.
-        /// </summary>
-        /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value>
-        public bool IsPremiere { get; set; }
-
-        /// <summary>
-        /// Gets or sets the official rating.
-        /// </summary>
-        /// <value>The official rating.</value>
-        public string OfficialRating { get; set; }
-
-        /// <summary>
-        /// Gets or sets the community rating.
-        /// </summary>
-        /// <value>The community rating.</value>
-        public float? CommunityRating { get; set; }
-
-        /// <summary>
-        /// Gets or sets the image path if it can be accessed directly from the file system.
-        /// </summary>
-        /// <value>The image path.</value>
-        public string ImagePath { get; set; }
-
-        /// <summary>
-        /// Gets or sets the image url if it can be downloaded.
-        /// </summary>
-        /// <value>The image URL.</value>
-        public string ImageUrl { get; set; }
-
-        /// <summary>
-        /// Gets or sets a value indicating whether this instance has image.
-        /// </summary>
-        /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value>
-        public bool? HasImage { get; set; }
-
-        /// <summary>
-        /// Gets or sets the show identifier.
-        /// </summary>
-        /// <value>The show identifier.</value>
-        public string ShowId { get; set; }
-
-        /// <summary>
-        /// Gets or sets the date last updated.
-        /// </summary>
-        /// <value>The date last updated.</value>
-        public DateTime DateLastUpdated { get; set; }
-    }
-}

+ 0 - 16
MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs

@@ -1,16 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using MediaBrowser.Model.LiveTv;
-
-namespace MediaBrowser.Controller.LiveTv
-{
-    public class RecordingStatusChangedEventArgs : EventArgs
-    {
-        public string RecordingId { get; set; }
-
-        public RecordingStatus NewStatus { get; set; }
-    }
-}

+ 526 - 60
MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs

@@ -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;
-
-                // setup a relative small canvas_size for overlay_qsv/vaapi to reduce transfer overhead
-                var (overlayW, overlayH) = GetFixedOutputSize(inW, inH, reqW, reqH, reqMaxW, 1080);
+                var subtitleWidth = state.SubtitleStream?.Width;
+                var subtitleHeight = state.SubtitleStream?.Height;
 
-                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,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>

+ 2 - 1
MediaBrowser.Controller/Session/ISessionManager.cs

@@ -111,7 +111,8 @@ namespace MediaBrowser.Controller.Session
         /// Reports the session ended.
         /// </summary>
         /// <param name="sessionId">The session identifier.</param>
-        void ReportSessionEnded(string sessionId);
+        /// <returns>Task.</returns>
+        ValueTask ReportSessionEnded(string sessionId);
 
         /// <summary>
         /// Sends the general command.

+ 7 - 21
MediaBrowser.Controller/Session/SessionInfo.cs

@@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Session
     /// <summary>
     /// Class SessionInfo.
     /// </summary>
-    public sealed class SessionInfo : IAsyncDisposable, IDisposable
+    public sealed class SessionInfo : IAsyncDisposable
     {
         // 1 second
         private const long ProgressIncrement = 10000000;
@@ -374,26 +374,6 @@ namespace MediaBrowser.Controller.Session
             }
         }
 
-        /// <inheritdoc />
-        public void Dispose()
-        {
-            _disposed = true;
-
-            StopAutomaticProgress();
-
-            var controllers = SessionControllers.ToList();
-            SessionControllers = Array.Empty<ISessionController>();
-
-            foreach (var controller in controllers)
-            {
-                if (controller is IDisposable disposable)
-                {
-                    _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
-                    disposable.Dispose();
-                }
-            }
-        }
-
         public async ValueTask DisposeAsync()
         {
             _disposed = true;
@@ -401,6 +381,7 @@ namespace MediaBrowser.Controller.Session
             StopAutomaticProgress();
 
             var controllers = SessionControllers.ToList();
+            SessionControllers = Array.Empty<ISessionController>();
 
             foreach (var controller in controllers)
             {
@@ -409,6 +390,11 @@ namespace MediaBrowser.Controller.Session
                     _logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name);
                     await disposableAsync.DisposeAsync().ConfigureAwait(false);
                 }
+                else if (controller is IDisposable disposable)
+                {
+                    _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name);
+                    disposable.Dispose();
+                }
             }
         }
     }

+ 17 - 4
MediaBrowser.MediaEncoding/Encoder/EncoderValidator.cs

@@ -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[]>

+ 1 - 6
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -904,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);
 
@@ -959,10 +958,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
                         StopProcess(processWrapper, 1000);
                     }
                 }
-                finally
-                {
-                    _thumbnailResourcePool.Release();
-                }
 
                 var exitCode = ranToCompletion ? processWrapper.ExitCode ?? 0 : -1;
 

+ 4 - 0
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -742,6 +742,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"

+ 200 - 28
MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
@@ -18,7 +19,6 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
-using MediaBrowser.Controller.Session;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
@@ -198,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,
@@ -453,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>

+ 2 - 1
MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs

@@ -11,6 +11,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using AsyncKeyedLock;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
@@ -401,7 +402,7 @@ public sealed class TranscodeManager : ITranscodeManager, IDisposable
 
         if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
         {
-            var user = userId.Equals(default) ? null : _userManager.GetUserById(userId);
+            var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId);
             if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
             {
                 this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state);

+ 2 - 1
MediaBrowser.Model/Dlna/StreamBuilder.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.MediaInfo;
@@ -1536,7 +1537,7 @@ namespace MediaBrowser.Model.Dlna
 
         private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource)
         {
-            if (options.ItemId.Equals(default))
+            if (options.ItemId.IsEmpty())
             {
                 ArgumentException.ThrowIfNullOrEmpty(options.DeviceId);
             }

+ 0 - 2
MediaBrowser.Model/IO/IStreamHelper.cs

@@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO
 
         Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken);
 
-        Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken);
-
         Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken);
     }
 }

+ 0 - 12
MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs

@@ -1,12 +0,0 @@
-#pragma warning disable CS1591
-
-namespace MediaBrowser.Model.LiveTv
-{
-    public enum LiveTvTunerStatus
-    {
-        Available = 0,
-        Disabled = 1,
-        RecordingTv = 2,
-        LiveTv = 3
-    }
-}

+ 6 - 1
MediaBrowser.Model/Session/HardwareEncodingType.cs

@@ -33,6 +33,11 @@
         /// <summary>
         /// Video ToolBox.
         /// </summary>
-        VideoToolBox = 5
+        VideoToolBox = 5,
+
+        /// <summary>
+        /// Rockchip Media Process Platform (RKMPP).
+        /// </summary>
+        RKMPP = 6
     }
 }

+ 2 - 2
MediaBrowser.Providers/Manager/ProviderManager.cs

@@ -706,7 +706,7 @@ namespace MediaBrowser.Providers.Manager
         {
             BaseItem? referenceItem = null;
 
-            if (!searchInfo.ItemId.Equals(default))
+            if (!searchInfo.ItemId.IsEmpty())
             {
                 referenceItem = _libraryManager.GetItemById(searchInfo.ItemId);
             }
@@ -944,7 +944,7 @@ namespace MediaBrowser.Providers.Manager
         public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority)
         {
             ArgumentNullException.ThrowIfNull(itemId);
-            if (itemId.Equals(default))
+            if (itemId.IsEmpty())
             {
                 throw new ArgumentException("Guid can't be empty", nameof(itemId));
             }

+ 22 - 4
MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs

@@ -460,10 +460,28 @@ namespace MediaBrowser.XbmcMetadata.Parsers
                     var trailer = reader.ReadNormalizedString();
                     if (!string.IsNullOrEmpty(trailer))
                     {
-                        item.AddTrailerUrl(trailer.Replace(
-                            "plugin://plugin.video.youtube/?action=play_video&videoid=",
-                            BaseNfoSaver.YouTubeWatchUrl,
-                            StringComparison.OrdinalIgnoreCase));
+                        if (trailer.StartsWith("plugin://plugin.video.youtube/?action=play_video&videoid=", StringComparison.OrdinalIgnoreCase))
+                        {
+                            // Deprecated format
+                            item.AddTrailerUrl(trailer.Replace(
+                                "plugin://plugin.video.youtube/?action=play_video&videoid=",
+                                BaseNfoSaver.YouTubeWatchUrl,
+                                StringComparison.OrdinalIgnoreCase));
+
+                            var suggestedUrl = trailer.Replace(
+                                "plugin://plugin.video.youtube/?action=play_video&videoid=",
+                                "plugin://plugin.video.youtube/play/?video_id=",
+                                StringComparison.OrdinalIgnoreCase);
+                            Logger.LogWarning("Trailer URL uses a deprecated format : {Url}. Using {NewUrl} instead is advised.", trailer, suggestedUrl);
+                        }
+                        else if (trailer.StartsWith("plugin://plugin.video.youtube/play/?video_id=", StringComparison.OrdinalIgnoreCase))
+                        {
+                            // Proper format
+                            item.AddTrailerUrl(trailer.Replace(
+                                "plugin://plugin.video.youtube/play/?video_id=",
+                                BaseNfoSaver.YouTubeWatchUrl,
+                                StringComparison.OrdinalIgnoreCase));
+                        }
                     }
 
                     break;

Some files were not shown because too many files changed in this diff