Przeglądaj źródła

update to current master to resolve merge conflict

herby2212 1 rok temu
rodzic
commit
27ceee8b6c
100 zmienionych plików z 3165 dodań i 918 usunięć
  1. 12 0
      .config/dotnet-tools.json
  2. 4 4
      .github/workflows/codeql-analysis.yml
  3. 7 7
      .github/workflows/commands.yml
  4. 4 4
      .github/workflows/openapi.yml
  5. 2 2
      .github/workflows/repo-bump-version.yaml
  6. 1 1
      .github/workflows/repo-stale.yaml
  7. 2 0
      CONTRIBUTORS.md
  8. 12 12
      Directory.Packages.props
  9. 1 1
      Emby.Dlna/DlnaManager.cs
  10. 69 0
      Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
  11. 17 117
      Emby.Dlna/Main/DlnaEntryPoint.cs
  12. 4 8
      Emby.Dlna/PlayTo/Device.cs
  13. 28 27
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  14. 2 2
      Emby.Dlna/PlayTo/PlayToManager.cs
  15. 5 5
      Emby.Naming/Common/NamingOptions.cs
  16. 1 1
      Emby.Naming/ExternalFiles/ExternalPathParser.cs
  17. 3 4
      Emby.Naming/Video/StubResolver.cs
  18. 1 1
      Emby.Photos/PhotoProvider.cs
  19. 2 4
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  20. 8 86
      Emby.Server.Implementations/ApplicationHost.cs
  21. 6 3
      Emby.Server.Implementations/Channels/ChannelManager.cs
  22. 2 5
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  23. 10 1
      Emby.Server.Implementations/Dto/DtoService.cs
  24. 0 2
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  25. 22 13
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  26. 0 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  27. 0 1
      Emby.Server.Implementations/IO/FileRefresher.cs
  28. 5 3
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  29. 17 16
      Emby.Server.Implementations/Library/LibraryManager.cs
  30. 13 5
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  31. 10 5
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  32. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  33. 1 1
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  34. 4 5
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  35. 0 1
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  36. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  37. 9 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  38. 7 18
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  39. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  40. 30 32
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  41. 0 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  42. 0 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  43. 2 0
      Emby.Server.Implementations/Localization/Core/en-US.json
  44. 18 0
      Emby.Server.Implementations/Localization/Core/fo.json
  45. 35 28
      Emby.Server.Implementations/Localization/Core/is.json
  46. 38 38
      Emby.Server.Implementations/Localization/Core/lv.json
  47. 3 1
      Emby.Server.Implementations/Localization/Core/ml.json
  48. 1 0
      Emby.Server.Implementations/Localization/Core/si.json
  49. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  50. 20 17
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  51. 1 1
      Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
  52. 26 13
      Emby.Server.Implementations/Net/SocketFactory.cs
  53. 6 10
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  54. 13 5
      Emby.Server.Implementations/Plugins/PluginManager.cs
  55. 2 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  56. 104 0
      Emby.Server.Implementations/SystemManager.cs
  57. 13 9
      Emby.Server.Implementations/Udp/UdpServer.cs
  58. 3 5
      Emby.Server.Implementations/Updates/InstallationManager.cs
  59. 11 5
      Jellyfin.Api/Controllers/DlnaServerController.cs
  60. 28 20
      Jellyfin.Api/Controllers/DynamicHlsController.cs
  61. 5 4
      Jellyfin.Api/Controllers/HlsSegmentController.cs
  62. 19 24
      Jellyfin.Api/Controllers/ImageController.cs
  63. 3 3
      Jellyfin.Api/Controllers/LibraryController.cs
  64. 4 4
      Jellyfin.Api/Controllers/SubtitleController.cs
  65. 22 25
      Jellyfin.Api/Controllers/SystemController.cs
  66. 101 0
      Jellyfin.Api/Controllers/TrickplayController.cs
  67. 50 35
      Jellyfin.Api/Helpers/DynamicHlsHelper.cs
  68. 5 3
      Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
  69. 6 8
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  70. 1 1
      Jellyfin.Api/Helpers/TranscodingJobHelper.cs
  71. 0 2
      Jellyfin.Api/Jellyfin.Api.csproj
  72. 10 10
      Jellyfin.Api/Middleware/ExceptionMiddleware.cs
  73. 6 1
      Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs
  74. 75 0
      Jellyfin.Data/Entities/TrickplayInfo.cs
  75. 6 0
      Jellyfin.Data/Entities/User.cs
  76. 9 7
      Jellyfin.Networking/Extensions/NetworkExtensions.cs
  77. 157 87
      Jellyfin.Networking/Manager/NetworkManager.cs
  78. 0 1
      Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs
  79. 1 2
      Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs
  80. 5 0
      Jellyfin.Server.Implementations/JellyfinDbContext.cs
  81. 681 0
      Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
  82. 40 0
      Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs
  83. 654 0
      Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
  84. 29 0
      Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs
  85. 37 2
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  86. 18 0
      Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs
  87. 5 25
      Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
  88. 474 0
      Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
  89. 20 3
      Jellyfin.Server.Implementations/Users/UserManager.cs
  90. 3 0
      Jellyfin.Server/CoreAppHost.cs
  91. 1 1
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  92. 1 2
      Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs
  93. 2 1
      Jellyfin.Server/Migrations/MigrationRunner.cs
  94. 55 0
      Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs
  95. 0 1
      Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
  96. 2 18
      Jellyfin.Server/Startup.cs
  97. 4 56
      MediaBrowser.Common/Extensions/ProcessExtensions.cs
  98. 4 20
      MediaBrowser.Common/IApplicationHost.cs
  99. 0 4
      MediaBrowser.Common/MediaBrowser.Common.csproj
  100. 0 1
      MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs

+ 12 - 0
.config/dotnet-tools.json

@@ -0,0 +1,12 @@
+{
+  "version": 1,
+  "isRoot": true,
+  "tools": {
+    "dotnet-ef": {
+      "version": "7.0.12",
+      "commands": [
+        "dotnet-ef"
+      ]
+    }
+  }
+}

+ 4 - 4
.github/workflows/codeql-analysis.yml

@@ -20,18 +20,18 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
     - name: Setup .NET
     - name: Setup .NET
       uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
       uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
       with:
       with:
         dotnet-version: '7.0.x'
         dotnet-version: '7.0.x'
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+      uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         queries: +security-extended
         queries: +security-extended
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+      uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
+      uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3

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

@@ -17,14 +17,14 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Notify as seen
       - name: Notify as seen
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           comment-id: ${{ github.event.comment.id }}
           comment-id: ${{ github.event.comment.id }}
           reactions: '+1'
           reactions: '+1'
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Notify as seen
       - name: Notify as seen
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ github.event.comment != null }}
         if: ${{ github.event.comment != null }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
           reactions: eyes
           reactions: eyes
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0
 
 
       - name: Notify as running
       - name: Notify as running
         id: comment_running
         id: comment_running
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ github.event.comment != null }}
         if: ${{ github.event.comment != null }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
           exit ${retcode}
           exit ${retcode}
 
 
       - name: Notify with result success
       - name: Notify with result success
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ github.event.comment != null && success() }}
         if: ${{ github.event.comment != null && success() }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
           reactions: hooray
           reactions: hooray
 
 
       - name: Notify with result failure
       - name: Notify with result failure
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ github.event.comment != null && failure() }}
         if: ${{ github.event.comment != null && failure() }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}

+ 4 - 4
.github/workflows/openapi.yml

@@ -14,7 +14,7 @@ jobs:
     permissions: read-all
     permissions: read-all
     steps:
     steps:
       - name: Checkout repository
       - name: Checkout repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
     permissions: read-all
     permissions: read-all
     steps:
     steps:
       - name: Checkout repository
       - name: Checkout repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -112,7 +112,7 @@ jobs:
           direction: last
           direction: last
           body-includes: openapi-diff-workflow-comment
           body-includes: openapi-diff-workflow-comment
       - name: Reply or edit difference comment (changed)
       - name: Reply or edit difference comment (changed)
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ steps.read-diff.outputs.body != '' }}
         if: ${{ steps.read-diff.outputs.body != '' }}
         with:
         with:
           issue-number: ${{ github.event.pull_request.number }}
           issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
 
 
             </details>
             </details>
       - name: Edit difference comment (unchanged)
       - name: Edit difference comment (unchanged)
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
         if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
         with:
         with:
           issue-number: ${{ github.event.pull_request.number }}
           issue-number: ${{ github.event.pull_request.number }}

+ 2 - 2
.github/workflows/repo-bump-version.yaml

@@ -33,7 +33,7 @@ jobs:
           yq-version: v4.9.8
           yq-version: v4.9.8
 
 
       - name: Checkout Repository
       - name: Checkout Repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           ref: ${{ env.TAG_BRANCH }}
           ref: ${{ env.TAG_BRANCH }}
 
 
@@ -66,7 +66,7 @@ jobs:
       NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
       NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
     steps:
     steps:
       - name: Checkout Repository
       - name: Checkout Repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
         with:
           ref: ${{ env.TAG_BRANCH }}
           ref: ${{ env.TAG_BRANCH }}
 
 

+ 1 - 1
.github/workflows/repo-stale.yaml

@@ -16,7 +16,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
     steps:
-      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
+      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
         with:
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           days-before-stale: 120
           days-before-stale: 120

+ 2 - 0
CONTRIBUTORS.md

@@ -57,6 +57,7 @@
  - [hawken93](https://github.com/hawken93)
  - [hawken93](https://github.com/hawken93)
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [ikomhoog](https://github.com/ikomhoog)
  - [ikomhoog](https://github.com/ikomhoog)
+ - [iwalton3](https://github.com/iwalton3)
  - [jftuga](https://github.com/jftuga)
  - [jftuga](https://github.com/jftuga)
  - [jmshrv](https://github.com/jmshrv)
  - [jmshrv](https://github.com/jmshrv)
  - [joern-h](https://github.com/joern-h)
  - [joern-h](https://github.com/joern-h)
@@ -88,6 +89,7 @@
  - [neilsb](https://github.com/neilsb)
  - [neilsb](https://github.com/neilsb)
  - [nevado](https://github.com/nevado)
  - [nevado](https://github.com/nevado)
  - [Nickbert7](https://github.com/Nickbert7)
  - [Nickbert7](https://github.com/Nickbert7)
+ - [nicknsy](https://github.com/nicknsy)
  - [nvllsvm](https://github.com/nvllsvm)
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [OancaAndrei](https://github.com/OancaAndrei)
  - [OancaAndrei](https://github.com/OancaAndrei)

+ 12 - 12
Directory.Packages.props

@@ -17,7 +17,7 @@
     <PackageVersion Include="Diacritics" Version="3.3.18" />
     <PackageVersion Include="Diacritics" Version="3.3.18" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
-    <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
+    <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" />
     <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
     <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
     <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
     <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
@@ -25,15 +25,15 @@
     <PackageVersion Include="libse" Version="3.6.13" />
     <PackageVersion Include="libse" Version="3.6.13" />
     <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="LrcParser" Version="2023.524.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
-    <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@@ -42,8 +42,8 @@
     <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
     <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
     <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
@@ -86,8 +86,8 @@
     <PackageVersion Include="TMDbLib" Version="2.0.0" />
     <PackageVersion Include="TMDbLib" Version="2.0.0" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
-    <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
     <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
     <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
-    <PackageVersion Include="xunit" Version="2.5.1" />
+    <PackageVersion Include="xunit" Version="2.5.3" />
   </ItemGroup>
   </ItemGroup>
 </Project>
 </Project>

+ 1 - 1
Emby.Dlna/DlnaManager.cs

@@ -228,7 +228,7 @@ namespace Emby.Dlna
             try
             try
             {
             {
                 return _fileSystem.GetFilePaths(path)
                 return _fileSystem.GetFilePaths(path)
-                    .Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
+                    .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
                     .Select(i => ParseProfileFile(i, type))
                     .Select(i => ParseProfileFile(i, type))
                     .Where(i => i is not null)
                     .Where(i => i is not null)
                     .ToList()!; // We just filtered out all the nulls
                     .ToList()!; // We just filtered out all the nulls

+ 69 - 0
Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Globalization;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using Emby.Dlna.ConnectionManager;
+using Emby.Dlna.ContentDirectory;
+using Emby.Dlna.MediaReceiverRegistrar;
+using Emby.Dlna.Ssdp;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Dlna;
+using MediaBrowser.Model.Net;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Rssdp.Infrastructure;
+
+namespace Emby.Dlna.Extensions;
+
+/// <summary>
+/// Extension methods for adding DLNA services.
+/// </summary>
+public static class DlnaServiceCollectionExtensions
+{
+    /// <summary>
+    /// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
+    public static void AddDlnaServices(
+        this IServiceCollection services,
+        IServerApplicationHost applicationHost)
+    {
+        services.AddHttpClient(NamedClient.Dlna, c =>
+            {
+                c.DefaultRequestHeaders.UserAgent.ParseAdd(
+                    string.Format(
+                        CultureInfo.InvariantCulture,
+                        "{0}/{1} UPnP/1.0 {2}/{3}",
+                        Environment.OSVersion.Platform,
+                        Environment.OSVersion,
+                        applicationHost.Name,
+                        applicationHost.ApplicationVersionString));
+
+                c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
+                c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
+            })
+            .ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
+            {
+                AutomaticDecompression = DecompressionMethods.All,
+                RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
+            });
+
+        services.AddSingleton<IDlnaManager, DlnaManager>();
+        services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+        services.AddSingleton<IContentDirectory, ContentDirectoryService>();
+        services.AddSingleton<IConnectionManager, ConnectionManagerService>();
+        services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
+
+        services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
+            provider.GetRequiredService<ISocketFactory>(),
+            provider.GetRequiredService<INetworkManager>(),
+            provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
+        {
+            IsShared = true
+        });
+    }
+}

+ 17 - 117
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Rssdp;
 using Rssdp;
 using Rssdp.Infrastructure;
 using Rssdp.Infrastructure;
@@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IDeviceDiscovery _deviceDiscovery;
         private readonly IDeviceDiscovery _deviceDiscovery;
-        private readonly ISocketFactory _socketFactory;
+        private readonly ISsdpCommunicationsServer _communicationsServer;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
-        private readonly object _syncLock = new object();
+        private readonly object _syncLock = new();
         private readonly bool _disabled;
         private readonly bool _disabled;
 
 
         private PlayToManager _manager;
         private PlayToManager _manager;
         private SsdpDevicePublisher _publisher;
         private SsdpDevicePublisher _publisher;
-        private ISsdpCommunicationsServer _communicationsServer;
 
 
         private bool _disposed;
         private bool _disposed;
 
 
@@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             IDeviceDiscovery deviceDiscovery,
             IDeviceDiscovery deviceDiscovery,
             IMediaEncoder mediaEncoder,
             IMediaEncoder mediaEncoder,
-            ISocketFactory socketFactory,
-            INetworkManager networkManager,
-            IUserViewManager userViewManager,
-            ITVSeriesManager tvSeriesManager)
+            ISsdpCommunicationsServer communicationsServer,
+            INetworkManager networkManager)
         {
         {
             _config = config;
             _config = config;
             _appHost = appHost;
             _appHost = appHost;
@@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _deviceDiscovery = deviceDiscovery;
             _deviceDiscovery = deviceDiscovery;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
-            _socketFactory = socketFactory;
+            _communicationsServer = communicationsServer;
             _networkManager = networkManager;
             _networkManager = networkManager;
             _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
             _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
 
-            ContentDirectory = new ContentDirectory.ContentDirectoryService(
-                dlnaManager,
-                userDataManager,
-                imageProcessor,
-                libraryManager,
-                config,
-                userManager,
-                loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
-                httpClientFactory,
-                localizationManager,
-                mediaSourceManager,
-                userViewManager,
-                mediaEncoder,
-                tvSeriesManager);
-
-            ConnectionManager = new ConnectionManager.ConnectionManagerService(
-                dlnaManager,
-                config,
-                loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
-                httpClientFactory);
-
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
-                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
-                httpClientFactory,
-                config);
-            Current = this;
-
             var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
             var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
             _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
             _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
 
 
@@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        public static DlnaEntryPoint Current { get; private set; }
-
-        /// <summary>
-        /// Gets a value indicating whether the dlna server is enabled.
-        /// </summary>
-        public static bool Enabled { get; private set; }
-
-        public IContentDirectory ContentDirectory { get; private set; }
-
-        public IConnectionManager ConnectionManager { get; private set; }
-
-        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
         public async Task RunAsync()
         public async Task RunAsync()
         {
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
         private void ReloadComponents()
         private void ReloadComponents()
         {
         {
             var options = _config.GetDlnaConfiguration();
             var options = _config.GetDlnaConfiguration();
-            Enabled = options.EnableServer;
-
-            StartSsdpHandler();
+            StartDeviceDiscovery();
 
 
             if (options.EnableServer)
             if (options.EnableServer)
             {
             {
@@ -195,36 +148,11 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        private void StartSsdpHandler()
-        {
-            try
-            {
-                if (_communicationsServer is null)
-                {
-                    var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
-
-                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
-                    {
-                        IsShared = true
-                    };
-
-                    StartDeviceDiscovery(_communicationsServer);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error starting ssdp handlers");
-            }
-        }
-
-        private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
+        private void StartDeviceDiscovery()
         {
         {
             try
             try
             {
             {
-                if (communicationsServer is not null)
-                {
-                    ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
-                }
+                ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -232,19 +160,6 @@ namespace Emby.Dlna.Main
             }
             }
         }
         }
 
 
-        private void DisposeDeviceDiscovery()
-        {
-            try
-            {
-                _logger.LogInformation("Disposing DeviceDiscovery");
-                ((DeviceDiscovery)_deviceDiscovery).Dispose();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error stopping device discovery");
-            }
-        }
-
         public void StartDevicePublisher(Configuration.DlnaOptions options)
         public void StartDevicePublisher(Configuration.DlnaOptions options)
         {
         {
             if (_publisher is not null)
             if (_publisher is not null)
@@ -317,7 +232,7 @@ namespace Emby.Dlna.Main
                     // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                     // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                 };
                 };
 
 
-                SetProperies(device, fullService);
+                SetProperties(device, fullService);
                 _publisher.AddDevice(device);
                 _publisher.AddDevice(device);
 
 
                 var embeddedDevices = new[]
                 var embeddedDevices = new[]
@@ -338,13 +253,13 @@ namespace Emby.Dlna.Main
                         // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                         // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                     };
                     };
 
 
-                    SetProperies(embeddedDevice, subDevice);
+                    SetProperties(embeddedDevice, subDevice);
                     device.AddDevice(embeddedDevice);
                     device.AddDevice(embeddedDevice);
                 }
                 }
             }
             }
         }
         }
 
 
-        private string CreateUuid(string text)
+        private static string CreateUuid(string text)
         {
         {
             if (!Guid.TryParse(text, out var guid))
             if (!Guid.TryParse(text, out var guid))
             {
             {
@@ -354,15 +269,14 @@ namespace Emby.Dlna.Main
             return guid.ToString("D", CultureInfo.InvariantCulture);
             return guid.ToString("D", CultureInfo.InvariantCulture);
         }
         }
 
 
-        private void SetProperies(SsdpDevice device, string fullDeviceType)
+        private static void SetProperties(SsdpDevice device, string fullDeviceType)
         {
         {
-            var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
-
-            var serviceParts = service.Split(':');
-
-            var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
+            var serviceParts = fullDeviceType
+                .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Split(':');
 
 
-            device.DeviceTypeNamespace = deviceTypeNamespace;
+            device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
             device.DeviceClass = serviceParts[1];
             device.DeviceClass = serviceParts[1];
             device.DeviceType = serviceParts[2];
             device.DeviceType = serviceParts[2];
         }
         }
@@ -443,20 +357,6 @@ namespace Emby.Dlna.Main
 
 
             DisposeDevicePublisher();
             DisposeDevicePublisher();
             DisposePlayToManager();
             DisposePlayToManager();
-            DisposeDeviceDiscovery();
-
-            if (_communicationsServer is not null)
-            {
-                _logger.LogInformation("Disposing SsdpCommunicationsServer");
-                _communicationsServer.Dispose();
-                _communicationsServer = null;
-            }
-
-            ContentDirectory = null;
-            ConnectionManager = null;
-            MediaReceiverRegistrar = null;
-            Current = null;
-
             _disposed = true;
             _disposed = true;
         }
         }
     }
     }

+ 4 - 8
Emby.Dlna/PlayTo/Device.cs

@@ -927,14 +927,11 @@ namespace Emby.Dlna.PlayTo
 
 
             var resElement = container.Element(UPnpNamespaces.Res);
             var resElement = container.Element(UPnpNamespaces.Res);
 
 
-            if (resElement is not null)
-            {
-                var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
+            var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
 
 
-                if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
-                {
-                    return info.Value.Split(':');
-                }
+            if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
+            {
+                return info.Value.Split(':');
             }
             }
 
 
             return new string[4];
             return new string[4];
@@ -1139,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
             return new Device(deviceProperties, httpClientFactory, logger);
             return new Device(deviceProperties, httpClientFactory, logger);
         }
         }
 
 
-#nullable enable
         private static DeviceIcon CreateIcon(XElement element)
         private static DeviceIcon CreateIcon(XElement element)
         {
         {
             ArgumentNullException.ThrowIfNull(element);
             ArgumentNullException.ThrowIfNull(element);

+ 28 - 27
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo
             var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
             var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
             using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
             using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
             response.EnsureSuccessStatusCode();
             response.EnsureSuccessStatusCode();
-            await using MemoryStream ms = new MemoryStream();
-            await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
-            ms.Position = 0;
-            try
+            Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            await using (stream.ConfigureAwait(false))
             {
             {
-                return await XDocument.LoadAsync(
-                    ms,
-                    LoadOptions.None,
-                    cancellationToken).ConfigureAwait(false);
-            }
-            catch (XmlException)
-            {
-                // try correcting the Xml response with common errors
-                ms.Position = 0;
-                using StreamReader sr = new StreamReader(ms);
-                var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
-
-                // find and replace unescaped ampersands (&)
-                xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
-
                 try
                 try
                 {
                 {
-                    // retry reading Xml
-                    using var xmlReader = new StringReader(xmlString);
                     return await XDocument.LoadAsync(
                     return await XDocument.LoadAsync(
-                        xmlReader,
+                        stream,
                         LoadOptions.None,
                         LoadOptions.None,
                         cancellationToken).ConfigureAwait(false);
                         cancellationToken).ConfigureAwait(false);
                 }
                 }
-                catch (XmlException ex)
+                catch (XmlException)
                 {
                 {
-                    _logger.LogError(ex, "Failed to parse response");
-                    _logger.LogDebug("Malformed response: {Content}\n", xmlString);
-
-                    return null;
+                    // try correcting the Xml response with common errors
+                    stream.Position = 0;
+                    using StreamReader sr = new StreamReader(stream);
+                    var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+
+                    // find and replace unescaped ampersands (&)
+                    xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
+
+                    try
+                    {
+                        // retry reading Xml
+                        using var xmlReader = new StringReader(xmlString);
+                        return await XDocument.LoadAsync(
+                            xmlReader,
+                            LoadOptions.None,
+                            cancellationToken).ConfigureAwait(false);
+                    }
+                    catch (XmlException ex)
+                    {
+                        _logger.LogError(ex, "Failed to parse response");
+                        _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+
+                        return null;
+                    }
                 }
                 }
             }
             }
         }
         }

+ 2 - 2
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -39,9 +39,9 @@ namespace Emby.Dlna.PlayTo
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IMediaEncoder _mediaEncoder;
 
 
+        private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
+        private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         private bool _disposed;
         private bool _disposed;
-        private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
 
 
         public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         {
         {

+ 5 - 5
Emby.Naming/Common/NamingOptions.cs

@@ -318,7 +318,7 @@ namespace Emby.Naming.Common
                 new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
                 new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
                 // <!-- foo.E01., foo.e01. -->
                 // <!-- foo.E01., foo.e01. -->
                 new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
                 new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
-                new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
+                new EpisodeExpression("(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
                 {
                 {
                     DateTimeFormats = new[]
                     DateTimeFormats = new[]
                     {
                     {
@@ -328,7 +328,7 @@ namespace Emby.Naming.Common
                         "yyyy MM dd"
                         "yyyy MM dd"
                     }
                     }
                 },
                 },
-                new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
+                new EpisodeExpression("(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
                 {
                 {
                     DateTimeFormats = new[]
                     DateTimeFormats = new[]
                     {
                     {
@@ -376,7 +376,7 @@ namespace Emby.Naming.Common
                     IsNamed = true,
                     IsNamed = true,
                     SupportsAbsoluteEpisodeNumbers = false
                     SupportsAbsoluteEpisodeNumbers = false
                 },
                 },
-                new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
+                new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
                 {
                 {
                     SupportsAbsoluteEpisodeNumbers = true
                     SupportsAbsoluteEpisodeNumbers = true
                 },
                 },
@@ -417,7 +417,7 @@ namespace Emby.Naming.Common
                 },
                 },
 
 
                 // "1-12 episode title"
                 // "1-12 episode title"
-                new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
+                new EpisodeExpression("([0-9]+)-([0-9]+)"),
 
 
                 // "01 - blah.avi", "01-blah.avi"
                 // "01 - blah.avi", "01-blah.avi"
                 new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
                 new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@@ -712,7 +712,7 @@ namespace Emby.Naming.Common
                 // Chapter is often beginning of filename
                 // Chapter is often beginning of filename
                 "^(?<chapter>[0-9]+)",
                 "^(?<chapter>[0-9]+)",
                 // Part if often ending of filename
                 // Part if often ending of filename
-                @"(?<!ch(?:apter) )(?<part>[0-9]+)$",
+                "(?<!ch(?:apter) )(?<part>[0-9]+)$",
                 // Sometimes named as 0001_005 (chapter_part)
                 // Sometimes named as 0001_005 (chapter_part)
                 "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
                 "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
                 // Some audiobooks are ripped from cd's, and will be named by disk number.
                 // Some audiobooks are ripped from cd's, and will be named by disk number.

+ 1 - 1
Emby.Naming/ExternalFiles/ExternalPathParser.cs

@@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
                 return null;
                 return null;
             }
             }
 
 
-            var extension = Path.GetExtension(path);
+            var extension = Path.GetExtension(path.AsSpan());
             if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
                 && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
             {
             {

+ 3 - 4
Emby.Naming/Video/StubResolver.cs

@@ -26,19 +26,18 @@ namespace Emby.Naming.Video
                 return false;
                 return false;
             }
             }
 
 
-            var extension = Path.GetExtension(path);
+            var extension = Path.GetExtension(path.AsSpan());
 
 
             if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return false;
                 return false;
             }
             }
 
 
-            path = Path.GetFileNameWithoutExtension(path);
-            var token = Path.GetExtension(path).TrimStart('.');
+            var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
 
 
             foreach (var rule in options.StubTypes)
             foreach (var rule in options.StubTypes)
             {
             {
-                if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
+                if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     stubType = rule.StubType;
                     stubType = rule.StubType;
                     return true;
                     return true;

+ 1 - 1
Emby.Photos/PhotoProvider.cs

@@ -61,7 +61,7 @@ namespace Emby.Photos
             item.SetImagePath(ImageType.Primary, item.Path);
             item.SetImagePath(ImageType.Primary, item.Path);
 
 
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
             // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
-            if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
+            if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
             {
             {
                 try
                 try
                 {
                 {

+ 2 - 4
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
     /// </summary>
     /// </summary>
     public abstract class BaseApplicationPaths : IApplicationPaths
     public abstract class BaseApplicationPaths : IApplicationPaths
     {
     {
-        private string _dataPath;
-
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
         /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
         /// </summary>
         /// </summary>
@@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
             CachePath = cacheDirectoryPath;
             CachePath = cacheDirectoryPath;
             WebPath = webDirectoryPath;
             WebPath = webDirectoryPath;
 
 
-            _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
+            DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
         /// Gets the folder path to the data directory.
         /// Gets the folder path to the data directory.
         /// </summary>
         /// </summary>
         /// <value>The data directory.</value>
         /// <value>The data directory.</value>
-        public string DataPath => _dataPath;
+        public string DataPath { get; }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string VirtualDataPath => "%AppDataPath%";
         public string VirtualDataPath => "%AppDataPath%";

+ 8 - 86
Emby.Server.Implementations/ApplicationHost.cs

@@ -13,9 +13,7 @@ using System.Net;
 using System.Reflection;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Emby.Dlna;
 using Emby.Dlna.Main;
 using Emby.Dlna.Main;
-using Emby.Dlna.Ssdp;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 using Emby.Photos;
 using Emby.Photos;
 using Emby.Server.Implementations.Channels;
 using Emby.Server.Implementations.Channels;
@@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.ClientEvent;
 using MediaBrowser.Controller.ClientEvent;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
@@ -101,7 +97,6 @@ using Microsoft.AspNetCore.Mvc;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Prometheus.DotNetRuntime;
 using Prometheus.DotNetRuntime;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
@@ -133,7 +128,7 @@ namespace Emby.Server.Implementations
         /// <value>All concrete types.</value>
         /// <value>All concrete types.</value>
         private Type[] _allConcreteTypes;
         private Type[] _allConcreteTypes;
 
 
-        private bool _disposed = false;
+        private bool _disposed;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -184,26 +179,16 @@ namespace Emby.Server.Implementations
 
 
         public bool CoreStartupHasCompleted { get; private set; }
         public bool CoreStartupHasCompleted { get; private set; }
 
 
-        public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
-            && !_startupOptions.IsService
-            && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
         /// <summary>
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// </summary>
         /// </summary>
         public INetworkManager NetManager { get; private set; }
         public INetworkManager NetManager { get; private set; }
 
 
-        /// <summary>
-        /// Gets a value indicating whether this instance has changes that require the entire application to restart.
-        /// </summary>
-        /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
-        public bool HasPendingRestart { get; private set; }
-
         /// <inheritdoc />
         /// <inheritdoc />
-        public bool IsShuttingDown { get; private set; }
+        public bool HasPendingRestart { get; private set; }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public bool ShouldRestart { get; private set; }
+        public bool ShouldRestart { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the logger.
         /// Gets the logger.
@@ -461,7 +446,7 @@ namespace Emby.Server.Implementations
 
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 
 
-            NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+            NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
 
 
             // Initialize runtime stat collection
             // Initialize runtime stat collection
             if (ConfigurationManager.Configuration.EnableMetrics)
             if (ConfigurationManager.Configuration.EnableMetrics)
@@ -507,6 +492,8 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
             serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
             serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
             serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
 
 
+            serviceCollection.AddScoped<ISystemManager, SystemManager>();
+
             serviceCollection.AddSingleton<TmdbClientManager>();
             serviceCollection.AddSingleton<TmdbClientManager>();
 
 
             serviceCollection.AddSingleton(NetManager);
             serviceCollection.AddSingleton(NetManager);
@@ -572,8 +559,6 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
-
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -585,8 +570,6 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
-
             serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
             serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
 
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@@ -850,24 +833,6 @@ namespace Emby.Server.Implementations
             }
             }
         }
         }
 
 
-        /// <inheritdoc />
-        public void Restart()
-        {
-            ShouldRestart = true;
-            Shutdown();
-        }
-
-        /// <inheritdoc />
-        public void Shutdown()
-        {
-            Task.Run(async () =>
-            {
-                await Task.Delay(100).ConfigureAwait(false);
-                IsShuttingDown = true;
-                Resolve<IHostApplicationLifetime>().StopApplication();
-            });
-        }
-
         /// <summary>
         /// <summary>
         /// Gets the composable part assemblies.
         /// Gets the composable part assemblies.
         /// </summary>
         /// </summary>
@@ -923,49 +888,6 @@ namespace Emby.Server.Implementations
 
 
         protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
         protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
 
 
-        /// <summary>
-        /// Gets the system status.
-        /// </summary>
-        /// <param name="request">Where this request originated.</param>
-        /// <returns>SystemInfo.</returns>
-        public SystemInfo GetSystemInfo(HttpRequest request)
-        {
-            return new SystemInfo
-            {
-                HasPendingRestart = HasPendingRestart,
-                IsShuttingDown = IsShuttingDown,
-                Version = ApplicationVersionString,
-                WebSocketPortNumber = HttpPort,
-                CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
-                Id = SystemId,
-                ProgramDataPath = ApplicationPaths.ProgramDataPath,
-                WebPath = ApplicationPaths.WebPath,
-                LogPath = ApplicationPaths.LogDirectoryPath,
-                ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
-                InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
-                CachePath = ApplicationPaths.CachePath,
-                CanLaunchWebBrowser = CanLaunchWebBrowser,
-                TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                SupportsLibraryMonitor = true,
-                PackageName = _startupOptions.PackageName
-            };
-        }
-
-        public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
-        {
-            return new PublicSystemInfo
-            {
-                Version = ApplicationVersionString,
-                ProductName = ApplicationProductName,
-                Id = SystemId,
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
-            };
-        }
-
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string GetSmartApiUrl(IPAddress remoteAddr)
         public string GetSmartApiUrl(IPAddress remoteAddr)
         {
         {
@@ -983,7 +905,7 @@ namespace Emby.Server.Implementations
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string GetSmartApiUrl(HttpRequest request)
         public string GetSmartApiUrl(HttpRequest request)
         {
         {
-            // Return the host in the HTTP request as the API url
+            // Return the host in the HTTP request as the API URL if not configured otherwise
             if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
             if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
             {
             {
                 int? requestPort = request.Host.Port;
                 int? requestPort = request.Host.Port;
@@ -1018,7 +940,7 @@ namespace Emby.Server.Implementations
         public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
         public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
         {
         {
             // With an empty source, the port will be null
             // With an empty source, the port will be null
-            var smart = NetManager.GetBindAddress(ipAddress, out _, true);
+            var smart = NetManager.GetBindAddress(ipAddress, out _, false);
             var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
             var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
             int? port = !allowHttps ? HttpPort : null;
             int? port = !allowHttps ? HttpPort : null;
             return GetLocalApiUrl(smart, scheme, port);
             return GetLocalApiUrl(smart, scheme, port);

+ 6 - 3
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
 
 
             Directory.CreateDirectory(Path.GetDirectoryName(path));
             Directory.CreateDirectory(Path.GetDirectoryName(path));
 
 
-            await using FileStream createStream = File.Create(path);
-            await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+            FileStream createStream = File.Create(path);
+            await using (createStream.ConfigureAwait(false))
+            {
+                await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
+            }
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
 
 
                 if (info.People is not null && info.People.Count > 0)
                 if (info.People is not null && info.People.Count > 0)
                 {
                 {
-                    _libraryManager.UpdatePeople(item, info.People);
+                    await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
                 }
                 }
             }
             }
             else if (forceUpdate)
             else if (forceUpdate)

+ 2 - 5
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3540,10 +3540,7 @@ namespace Emby.Server.Implementations.Data
                         .Append(paramName)
                         .Append(paramName)
                         .Append("))) OR ");
                         .Append("))) OR ");
 
 
-                    if (statement is not null)
-                    {
-                        statement.TryBind(paramName, query.PersonIds[i]);
-                    }
+                    statement?.TryBind(paramName, query.PersonIds[i]);
                 }
                 }
 
 
                 clauseBuilder.Length -= Or.Length;
                 clauseBuilder.Length -= Or.Length;
@@ -4382,7 +4379,7 @@ namespace Emby.Server.Implementations.Data
 
 
                 foreach (var videoType in query.VideoTypes)
                 foreach (var videoType in query.VideoTypes)
                 {
                 {
-                    videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+                    videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
                 }
                 }
 
 
                 whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");
                 whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");

+ 10 - 1
Emby.Server.Implementations/Dto/DtoService.cs

@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 
 
         private readonly ILyricManager _lyricManager;
         private readonly ILyricManager _lyricManager;
+        private readonly ITrickplayManager _trickplayManager;
 
 
         public DtoService(
         public DtoService(
             ILogger<DtoService> logger,
             ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
             IApplicationHost appHost,
             IApplicationHost appHost,
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             Lazy<ILiveTvManager> livetvManagerFactory,
             Lazy<ILiveTvManager> livetvManagerFactory,
-            ILyricManager lyricManager)
+            ILyricManager lyricManager,
+            ITrickplayManager trickplayManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _livetvManagerFactory = livetvManagerFactory;
             _livetvManagerFactory = livetvManagerFactory;
             _lyricManager = lyricManager;
             _lyricManager = lyricManager;
+            _trickplayManager = trickplayManager;
         }
         }
 
 
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
                     dto.Chapters = _itemRepo.GetChapters(item);
                     dto.Chapters = _itemRepo.GetChapters(item);
                 }
                 }
 
 
+                if (options.ContainsField(ItemFields.Trickplay))
+                {
+                    dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+                }
+
                 if (video.ExtraType.HasValue)
                 if (video.ExtraType.HasValue)
                 {
                 {
                     dto.ExtraType = video.ExtraType.Value.ToString();
                     dto.ExtraType = video.ExtraType.Value.ToString();

+ 0 - 2
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -43,8 +43,6 @@
     <TargetFramework>net7.0</TargetFramework>
     <TargetFramework>net7.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
-    <NoWarn>AD0001</NoWarn>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 22 - 13
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.EntryPoints
 namespace Emby.Server.Implementations.EntryPoints
 {
 {
     /// <summary>
     /// <summary>
-    /// Class UdpServerEntryPoint.
+    /// Class responsible for registering all UDP broadcast endpoints and their handlers.
     /// </summary>
     /// </summary>
     public sealed class UdpServerEntryPoint : IServerEntryPoint
     public sealed class UdpServerEntryPoint : IServerEntryPoint
     {
     {
@@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IConfiguration _config;
         private readonly IConfiguration _config;
         private readonly IConfigurationManager _configurationManager;
         private readonly IConfigurationManager _configurationManager;
         private readonly INetworkManager _networkManager;
         private readonly INetworkManager _networkManager;
-        private readonly bool _enableMultiSocketBinding;
 
 
         /// <summary>
         /// <summary>
         /// The UDP server.
         /// The UDP server.
         /// </summary>
         /// </summary>
-        private List<UdpServer> _udpServers;
-        private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed = false;
+        private readonly List<UdpServer> _udpServers;
+        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+        private bool _disposed;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
             _configurationManager = configurationManager;
             _configurationManager = configurationManager;
             _networkManager = networkManager;
             _networkManager = networkManager;
             _udpServers = new List<UdpServer>();
             _udpServers = new List<UdpServer>();
-            _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
             try
             try
             {
             {
-                if (_enableMultiSocketBinding)
+                // Linux needs to bind to the broadcast addresses to get broadcast traffic
+                // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+                if (OperatingSystem.IsLinux())
                 {
                 {
-                    // Add global broadcast socket
+                    // Add global broadcast listener
                     var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
                     var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
                     server.Start(_cancellationTokenSource.Token);
                     server.Start(_cancellationTokenSource.Token);
                     _udpServers.Add(server);
                     _udpServers.Add(server);
 
 
-                    // Add bind address specific broadcast sockets
+                    // Add bind address specific broadcast listeners
                     // IPv6 is currently unsupported
                     // IPv6 is currently unsupported
                     var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
                     var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
                     foreach (var intf in validInterfaces)
                     foreach (var intf in validInterfaces)
@@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
                 }
                 }
                 else
                 else
                 {
                 {
-                    var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
-                    server.Start(_cancellationTokenSource.Token);
-                    _udpServers.Add(server);
+                    // Add bind address specific broadcast listeners
+                    // IPv6 is currently unsupported
+                    var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+                    foreach (var intf in validInterfaces)
+                    {
+                        var intfAddress = intf.Address;
+                        _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+                        var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+                        server.Start(_cancellationTokenSource.Token);
+                        _udpServers.Add(server);
+                    }
                 }
                 }
             }
             }
             catch (SocketException ex)
             catch (SocketException ex)
@@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
         {
         {
             if (_disposed)
             if (_disposed)
             {
             {
-                throw new ObjectDisposedException(this.GetType().Name);
+                throw new ObjectDisposedException(GetType().Name);
             }
             }
         }
         }
 
 

+ 0 - 1
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.HttpServer
 namespace Emby.Server.Implementations.HttpServer

+ 0 - 1
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
 
 
             DisposeTimer();
             DisposeTimer();
             _disposed = true;
             _disposed = true;
-            GC.SuppressFinalize(this);
         }
         }
     }
     }
 }
 }

+ 5 - 3
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
             }
             }
 
 
             // unc path
             // unc path
-            if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+            if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
             {
             {
                 return filePath;
                 return filePath;
             }
             }
@@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO
                 return filePath;
                 return filePath;
             }
             }
 
 
+            var filePathSpan = filePath.AsSpan();
+
             // relative path
             // relative path
             if (firstChar == '\\')
             if (firstChar == '\\')
             {
             {
-                filePath = filePath.Substring(1);
+                filePathSpan = filePathSpan.Slice(1);
             }
             }
 
 
             try
             try
             {
             {
-                return Path.GetFullPath(Path.Combine(folderPath, filePath));
+                return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
             }
             }
             catch (ArgumentException)
             catch (ArgumentException)
             {
             {

+ 17 - 16
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
         {
         {
             var path = Person.GetPath(name);
             var path = Person.GetPath(name);
             var id = GetItemByNameId<Person>(path);
             var id = GetItemByNameId<Person>(path);
-            if (GetItemById(id) is not Person item)
+            if (GetItemById(id) is Person item)
             {
             {
-                item = new Person
-                {
-                    Name = name,
-                    Id = id,
-                    DateCreated = DateTime.UtcNow,
-                    DateModified = DateTime.UtcNow,
-                    Path = path
-                };
+                return item;
             }
             }
 
 
-            return item;
+            return null;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
                 Name = Path.GetFileName(dir),
                 Name = Path.GetFileName(dir),
 
 
                 Locations = _fileSystem.GetFilePaths(dir, false)
                 Locations = _fileSystem.GetFilePaths(dir, false)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                     .Select(i =>
                     .Select(i =>
                     {
                     {
                         try
                         try
@@ -2858,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
                 {
                 {
                     var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
                     var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
 
 
-                    File.WriteAllBytes(path, Array.Empty<byte>());
+                    await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
                 }
                 }
 
 
                 CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
                 CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
                 var saveEntity = false;
                 var saveEntity = false;
                 var personEntity = GetPerson(person.Name);
                 var personEntity = GetPerson(person.Name);
 
 
-                // if PresentationUniqueKey is empty it's likely a new item.
-                if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+                if (personEntity is null)
                 {
                 {
+                    var path = Person.GetPath(person.Name);
+                    personEntity = new Person()
+                    {
+                        Name = person.Name,
+                        Id = GetItemByNameId<Person>(path),
+                        DateCreated = DateTime.UtcNow,
+                        DateModified = DateTime.UtcNow,
+                        Path = path
+                    };
+
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     saveEntity = true;
                     saveEntity = true;
                 }
                 }
@@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                 .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
                 .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
 
 
             if (!string.IsNullOrEmpty(shortcut))
             if (!string.IsNullOrEmpty(shortcut))

+ 13 - 5
Emby.Server.Implementations/Library/LiveStreamHelper.cs

@@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
 
 
             if (!string.IsNullOrEmpty(cacheKey))
             if (!string.IsNullOrEmpty(cacheKey))
             {
             {
+                FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                 try
                 try
                 {
                 {
-                    await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
 
 
                     // _logger.LogDebug("Found cached media info");
                     // _logger.LogDebug("Found cached media info");
                 }
                 }
-                catch
+                catch (Exception ex)
                 {
                 {
+                    _logger.LogError(ex, "Error deserializing mediainfo cache");
+                }
+                finally
+                {
+                    await jsonStream.DisposeAsync().ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
@@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
                 if (cacheFilePath is not null)
                 if (cacheFilePath is not null)
                 {
                 {
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
-                    await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
-                    await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                    FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
+                    await using (createStream.ConfigureAwait(false))
+                    {
+                        await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                    }
 
 
-                    // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
+                    _logger.LogDebug("Saved media info to {0}", cacheFilePath);
                 }
                 }
             }
             }
 
 

+ 10 - 5
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
 
 
             if (!string.IsNullOrEmpty(cacheKey))
             if (!string.IsNullOrEmpty(cacheKey))
             {
             {
+                FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                 try
                 try
                 {
                 {
-                    await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
                     mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
-                    // _logger.LogDebug("Found cached media info");
                 }
                 }
                 catch (Exception ex)
                 catch (Exception ex)
                 {
                 {
                     _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
                     _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
                 }
                 }
+                finally
+                {
+                    await jsonStream.DisposeAsync().ConfigureAwait(false);
+                }
             }
             }
 
 
             if (mediaInfo is null)
             if (mediaInfo is null)
@@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
                 if (cacheFilePath is not null)
                 if (cacheFilePath is not null)
                 {
                 {
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
-                    await using FileStream createStream = File.Create(cacheFilePath);
-                    await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                    FileStream createStream = File.Create(cacheFilePath);
+                    await using (createStream.ConfigureAwait(false))
+                    {
+                        await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                    }
 
 
                     // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
                     // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
                 }
                 }

+ 3 - 3
Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs

@@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
 
             if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
             if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
             {
             {
-                var extension = Path.GetExtension(args.Path);
+                var extension = Path.GetExtension(args.Path.AsSpan());
 
 
-                if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
+                if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
                 {
                 {
                     // if audio file exists of same name, return null
                     // if audio file exists of same name, return null
                     return null;
                     return null;
@@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
 
                 if (item is not null)
                 if (item is not null)
                 {
                 {
-                    item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
+                    item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
 
 
                     item.IsInMixedFolder = true;
                     item.IsInMixedFolder = true;
                 }
                 }

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 return false;
                 return false;
             }
             }
 
 
-            return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
+            return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 4 - 5
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
                 return GetBook(args);
                 return GetBook(args);
             }
             }
 
 
-            var extension = Path.GetExtension(args.Path);
+            var extension = Path.GetExtension(args.Path.AsSpan());
 
 
-            if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+            if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
             {
                 // It's a book
                 // It's a book
                 return new Book
                 return new Book
@@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
         {
         {
             var bookFiles = args.FileSystemChildren.Where(f =>
             var bookFiles = args.FileSystemChildren.Where(f =>
             {
             {
-                var fileExtension = Path.GetExtension(f.FullName)
-                    ?? string.Empty;
+                var fileExtension = Path.GetExtension(f.FullName.AsSpan());
 
 
                 return _validExtensions.Contains(
                 return _validExtensions.Contains(
                     fileExtension,
                     fileExtension,
-                    StringComparer.OrdinalIgnoreCase);
+                    StringComparison.OrdinalIgnoreCase);
             }).ToList();
             }).ToList();
 
 
             // Don't return a Book if there is more (or less) than one document in the directory
             // Don't return a Book if there is more (or less) than one document in the directory

+ 0 - 1
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Common;

+ 1 - 1
Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs

@@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                     var resolver = new Naming.TV.EpisodeResolver(namingOptions);
                     var resolver = new Naming.TV.EpisodeResolver(namingOptions);
 
 
                     var folderName = System.IO.Path.GetFileName(path);
                     var folderName = System.IO.Path.GetFileName(path);
-                    var testPath = "\\\\test\\" + folderName;
+                    var testPath = @"\\test\" + folderName;
 
 
                     var episodeInfo = resolver.Resolve(testPath, true);
                     var episodeInfo = resolver.Resolve(testPath, true);
 
 

+ 9 - 5
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 return;
                 return;
             }
             }
 
 
-            await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+            var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+            await using (stream.ConfigureAwait(false))
             {
             {
                 var settings = new XmlWriterSettings
                 var settings = new XmlWriterSettings
                 {
                 {
@@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     Async = true
                     Async = true
                 };
                 };
 
 
-                await using (var writer = XmlWriter.Create(stream, settings))
+                var writer = XmlWriter.Create(stream, settings);
+                await using (writer.ConfigureAwait(false))
                 {
                 {
                     await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
                     await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
                     await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
                     await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 return;
                 return;
             }
             }
 
 
-            await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
+            var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
+            await using (stream.ConfigureAwait(false))
             {
             {
                 var settings = new XmlWriterSettings
                 var settings = new XmlWriterSettings
                 {
                 {
@@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
 
                 var isSeriesEpisode = timer.IsProgramSeries;
                 var isSeriesEpisode = timer.IsProgramSeries;
 
 
-                await using (var writer = XmlWriter.Create(stream, settings))
+                var writer = XmlWriter.Create(stream, settings);
+                await using (writer.ConfigureAwait(false))
                 {
                 {
                     await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
                     await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
 
 
@@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     }
                     else
                     else
                     {
                     {
-                        await writer.WriteStartElementAsync(null, "movie", null);
+                        await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
 
 
                         if (!string.IsNullOrWhiteSpace(item.Name))
                         if (!string.IsNullOrWhiteSpace(item.Name))
                         {
                         {

+ 7 - 18
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             options.Content = JsonContent.Create(requestList, options: _jsonOptions);
             options.Content = JsonContent.Create(requestList, options: _jsonOptions);
             options.Headers.TryAddWithoutValidation("token", token);
             options.Headers.TryAddWithoutValidation("token", token);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
-            await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
             if (dailySchedules is null)
             if (dailySchedules is null)
             {
             {
                 return Array.Empty<ProgramInfo>();
                 return Array.Empty<ProgramInfo>();
@@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
             programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
 
 
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
             using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
-            await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
             if (programDetails is null)
             if (programDetails is null)
             {
             {
                 return Array.Empty<ProgramInfo>();
                 return Array.Empty<ProgramInfo>();
@@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             try
             try
             {
             {
                 using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
                 using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
-                await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
+                return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
             }
             }
             catch (Exception ex)
             catch (Exception ex)
             {
             {
@@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             try
             try
             {
             {
                 using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
                 using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
-                await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-
-                var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+                var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
                 if (root is not null)
                 if (root is not null)
                 {
                 {
                     foreach (HeadendsDto headend in root)
                     foreach (HeadendsDto headend in root)
@@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
 
             using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
             using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
             response.EnsureSuccessStatusCode();
             response.EnsureSuccessStatusCode();
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
             if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
             if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
             {
             {
                 _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
                 _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
             {
                 using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
                 using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
                 httpResponse.EnsureSuccessStatusCode();
                 httpResponse.EnsureSuccessStatusCode();
-                await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                using var response = httpResponse.Content;
-                var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
-
+                var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
                 return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
                 return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
             }
             }
             catch (HttpRequestException ex)
             catch (HttpRequestException ex)
@@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             options.Headers.TryAddWithoutValidation("token", token);
             options.Headers.TryAddWithoutValidation("token", token);
 
 
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
             using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
-            await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
+            var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
             if (root is null)
             if (root is null)
             {
             {
                 return new List<ChannelInfo>();
                 return new List<ChannelInfo>();

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 namespace Emby.Server.Implementations.LiveTv.TunerHosts

+ 30 - 32
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Text.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -76,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
 
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
-                .ConfigureAwait(false) ?? new List<Channels>();
-
+            var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
             if (info.ImportFavoritesOnly)
             if (info.ImportFavoritesOnly)
             {
             {
-                lineup = lineup.Where(i => i.Favorite).ToList();
+                lineup = lineup.Where(i => i.Favorite);
             }
             }
 
 
             return lineup.Where(i => !i.DRM).ToList();
             return lineup.Where(i => !i.DRM).ToList();
@@ -129,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                     .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                     .ConfigureAwait(false);
                     .ConfigureAwait(false);
                 response.EnsureSuccessStatusCode();
                 response.EnsureSuccessStatusCode();
-                await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
-                    .ConfigureAwait(false);
+                var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
 
 
                 if (!string.IsNullOrEmpty(cacheKey))
                 if (!string.IsNullOrEmpty(cacheKey))
                 {
                 {
@@ -175,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
                 .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                 .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
             var tuners = new List<LiveTvTunerInfo>();
             var tuners = new List<LiveTvTunerInfo>();
-            await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
+            var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            await using (stream.ConfigureAwait(false))
             {
             {
-                string stripedLine = StripXML(line);
-                if (stripedLine.Contains("Channel", StringComparison.Ordinal))
+                using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+                await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
                 {
                 {
-                    LiveTvTunerStatus status;
-                    var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
-                    var name = stripedLine.Substring(0, index - 1);
-                    var currentChannel = stripedLine.Substring(index + 7);
-                    if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+                    string stripedLine = StripXML(line);
+                    if (stripedLine.Contains("Channel", StringComparison.Ordinal))
                     {
                     {
-                        status = LiveTvTunerStatus.LiveTv;
-                    }
-                    else
-                    {
-                        status = LiveTvTunerStatus.Available;
-                    }
+                        LiveTvTunerStatus status;
+                        var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+                        var name = stripedLine.Substring(0, index - 1);
+                        var currentChannel = stripedLine.Substring(index + 7);
+                        if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+                        {
+                            status = LiveTvTunerStatus.LiveTv;
+                        }
+                        else
+                        {
+                            status = LiveTvTunerStatus.Available;
+                        }
 
 
-                    tuners.Add(new LiveTvTunerInfo
-                    {
-                        Name = name,
-                        SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
-                        ProgramName = currentChannel,
-                        Status = status
-                    });
+                        tuners.Add(new LiveTvTunerInfo
+                        {
+                            Name = name,
+                            SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+                            ProgramName = currentChannel,
+                            Status = status
+                        });
+                    }
                 }
                 }
             }
             }
 
 

+ 0 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs

@@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     StopStreaming(socket).GetAwaiter().GetResult();
                     StopStreaming(socket).GetAwaiter().GetResult();
                 }
                 }
             }
             }
-
-            GC.SuppressFinalize(this);
         }
         }
 
 
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)

+ 0 - 2
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -5,7 +5,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
-using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
 using System.Threading;
 using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 using Microsoft.Net.Http.Headers;
 
 

+ 2 - 0
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -112,6 +112,8 @@
     "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
     "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
     "TaskRefreshPeople": "Refresh People",
     "TaskRefreshPeople": "Refresh People",
     "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
     "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+    "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+    "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
     "TaskUpdatePlugins": "Update Plugins",
     "TaskUpdatePlugins": "Update Plugins",
     "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
     "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
     "TaskCleanTranscode": "Clean Transcode Directory",
     "TaskCleanTranscode": "Clean Transcode Directory",

+ 18 - 0
Emby.Server.Implementations/Localization/Core/fo.json

@@ -0,0 +1,18 @@
+{
+    "Artists": "Listafólk",
+    "Collections": "Søvn",
+    "Default": "Sjálvgildi",
+    "DeviceOfflineWithName": "{0} hevur slitið sambandið",
+    "External": "Ytri",
+    "Genres": "Greinar",
+    "Albums": "Album",
+    "AppDeviceValues": "App: {0}, Eind: {1}",
+    "Application": "Nýtsluskipan",
+    "Books": "Bøkur",
+    "Channels": "Rásir",
+    "ChapterNameValue": "Kapittul {0}",
+    "DeviceOnlineWithName": "{0} er sambundið",
+    "Favorites": "Yndis",
+    "Folders": "Mappur",
+    "Forced": "Kravt"
+}

+ 35 - 28
Emby.Server.Implementations/Localization/Core/is.json

@@ -13,8 +13,8 @@
     "HeaderFavoriteArtists": "Uppáhalds Listamenn",
     "HeaderFavoriteArtists": "Uppáhalds Listamenn",
     "HeaderFavoriteAlbums": "Uppáhalds Plötur",
     "HeaderFavoriteAlbums": "Uppáhalds Plötur",
     "HeaderContinueWatching": "Halda áfram að horfa",
     "HeaderContinueWatching": "Halda áfram að horfa",
-    "HeaderAlbumArtists": "Höfundur plötu",
-    "Genres": "Tegundir",
+    "HeaderAlbumArtists": "Listamaður á umslagi",
+    "Genres": "Stefnur",
     "Folders": "Möppur",
     "Folders": "Möppur",
     "Favorites": "Uppáhalds",
     "Favorites": "Uppáhalds",
     "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
     "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@@ -22,32 +22,32 @@
     "DeviceOfflineWithName": "{0} hefur aftengst",
     "DeviceOfflineWithName": "{0} hefur aftengst",
     "Collections": "Söfn",
     "Collections": "Söfn",
     "ChapterNameValue": "Kafli {0}",
     "ChapterNameValue": "Kafli {0}",
-    "Channels": "Stöðvar",
-    "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
+    "Channels": "Rásir",
+    "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
     "Books": "Bækur",
     "Books": "Bækur",
-    "AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
-    "Artists": "Listamaður",
+    "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
+    "Artists": "Listamenn",
     "Application": "Forrit",
     "Application": "Forrit",
     "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
     "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
     "Albums": "Plötur",
     "Albums": "Plötur",
-    "Plugin": "Viðbót",
-    "Photos": "Myndir",
-    "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
-    "NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
+    "Plugin": "Viðbótarvirkni",
+    "Photos": "Ljósmyndir",
+    "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
+    "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
     "NotificationOptionUserLockedOut": "Notandi læstur úti",
     "NotificationOptionUserLockedOut": "Notandi læstur úti",
-    "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
-    "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
-    "NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
-    "NotificationOptionPluginInstalled": "Viðbót sett upp",
+    "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
+    "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
+    "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
+    "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
     "NotificationOptionPluginError": "Bilun í viðbót",
     "NotificationOptionPluginError": "Bilun í viðbót",
     "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
     "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
-    "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
+    "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
     "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
     "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
     "NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
     "NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
     "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
     "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
     "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
     "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
-    "NameSeasonUnknown": "Sería óþekkt",
-    "NameSeasonNumber": "Sería {0}",
+    "NameSeasonUnknown": "Þáttaröð óþekkt",
+    "NameSeasonNumber": "Þáttaröð {0}",
     "MixedContent": "Blandað efni",
     "MixedContent": "Blandað efni",
     "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
     "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
     "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
     "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@@ -57,24 +57,24 @@
     "User": "Notandi",
     "User": "Notandi",
     "System": "Kerfi",
     "System": "Kerfi",
     "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
     "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
-    "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.",
+    "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
     "NameInstallFailed": "{0} uppsetning mistókst",
     "NameInstallFailed": "{0} uppsetning mistókst",
     "MusicVideos": "Tónlistarmyndbönd",
     "MusicVideos": "Tónlistarmyndbönd",
     "Music": "Tónlist",
     "Music": "Tónlist",
     "Movies": "Kvikmyndir",
     "Movies": "Kvikmyndir",
     "UserDeletedWithName": "Notanda {0} hefur verið eytt",
     "UserDeletedWithName": "Notanda {0} hefur verið eytt",
     "UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
     "UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
-    "TvShows": "Þættir",
+    "TvShows": "Sjónvarpsþættir",
     "Sync": "Samstilla",
     "Sync": "Samstilla",
     "Songs": "Lög",
     "Songs": "Lög",
-    "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
+    "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
     "ScheduledTaskStartedWithName": "{0} hafin",
     "ScheduledTaskStartedWithName": "{0} hafin",
     "ScheduledTaskFailedWithName": "{0} mistókst",
     "ScheduledTaskFailedWithName": "{0} mistókst",
     "PluginUpdatedWithName": "{0} var uppfært",
     "PluginUpdatedWithName": "{0} var uppfært",
     "PluginUninstalledWithName": "{0} var fjarlægt",
     "PluginUninstalledWithName": "{0} var fjarlægt",
     "PluginInstalledWithName": "{0} var sett upp",
     "PluginInstalledWithName": "{0} var sett upp",
     "NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
     "NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
-    "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.",
+    "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
     "VersionNumber": "Útgáfa {0}",
     "VersionNumber": "Útgáfa {0}",
     "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
     "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
     "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
     "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@@ -83,14 +83,14 @@
     "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
     "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
     "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
     "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
     "UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
     "UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
-    "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
-    "UserDownloadingItemWithValues": "{0} Hleður niður {1}",
+    "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
+    "UserDownloadingItemWithValues": "{0} hleður niður {1}",
     "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
     "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
-    "ProviderValue": "Veitandi: {0}",
+    "ProviderValue": "Efnisveita: {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
     "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
-    "ValueSpecialEpisodeName": "Sérstakt - {0}",
-    "Shows": "Sýningar",
-    "Playlists": "Spilunarlisti",
+    "ValueSpecialEpisodeName": "Sérstaktur - {0}",
+    "Shows": "Þættir",
+    "Playlists": "Efnisskrár",
     "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
     "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
     "TaskRefreshChannels": "Endurhlaða Rásir",
     "TaskRefreshChannels": "Endurhlaða Rásir",
     "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
     "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@@ -116,5 +116,12 @@
     "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
     "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
     "TaskCleanLogs": "Hreinsa færslu skrá",
     "TaskCleanLogs": "Hreinsa færslu skrá",
     "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
     "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
-    "HearingImpaired": "Heyrnarskertur"
+    "HearingImpaired": "Heyrnarskertur",
+    "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
+    "TaskKeyframeExtractor": "Lykilrammaplokkari",
+    "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
+    "TaskRefreshChapterImages": "Plokka kafla-myndir",
+    "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
+    "Forced": "Þvingað",
+    "External": "Útvær"
 }
 }

+ 38 - 38
Emby.Server.Implementations/Localization/Core/lv.json

@@ -1,7 +1,7 @@
 {
 {
     "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
     "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
     "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
     "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
-    "HeaderRecordingGroups": "Ierakstu Grupas",
+    "HeaderRecordingGroups": "Ierakstu grupas",
     "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
     "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
     "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
     "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
     "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
     "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@@ -14,7 +14,7 @@
     "Photos": "Attēli",
     "Photos": "Attēli",
     "NotificationOptionUserLockedOut": "Lietotājs bloķēts",
     "NotificationOptionUserLockedOut": "Lietotājs bloķēts",
     "LabelRunningTimeValue": "Garums: {0}",
     "LabelRunningTimeValue": "Garums: {0}",
-    "Inherit": "Mantot",
+    "Inherit": "Pārmantot",
     "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
     "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
     "VersionNumber": "Versija {0}",
     "VersionNumber": "Versija {0}",
     "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
     "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
@@ -28,7 +28,7 @@
     "UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
     "UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
     "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
     "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
     "User": "Lietotājs",
     "User": "Lietotājs",
-    "TvShows": "TV Raidījumi",
+    "TvShows": "TV raidījumi",
     "Sync": "Sinhronizācija",
     "Sync": "Sinhronizācija",
     "System": "Sistēma",
     "System": "Sistēma",
     "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
     "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
@@ -38,11 +38,11 @@
     "PluginUninstalledWithName": "{0} tika noņemts",
     "PluginUninstalledWithName": "{0} tika noņemts",
     "PluginInstalledWithName": "{0} tika uzstādīts",
     "PluginInstalledWithName": "{0} tika uzstādīts",
     "Plugin": "Paplašinājums",
     "Plugin": "Paplašinājums",
-    "Playlists": "Atskaņošanas Saraksti",
+    "Playlists": "Atskaņošanas saraksti",
     "MixedContent": "Jaukts saturs",
     "MixedContent": "Jaukts saturs",
-    "HomeVideos": "Mājas Video",
+    "HomeVideos": "Mājas video",
     "HeaderNextUp": "Nākamais",
     "HeaderNextUp": "Nākamais",
-    "ChapterNameValue": "Nodaļa {0}",
+    "ChapterNameValue": "{0}. nodaļa",
     "Application": "Lietotne",
     "Application": "Lietotne",
     "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
     "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
     "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
     "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
@@ -56,14 +56,14 @@
     "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
     "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
     "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
     "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
     "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
     "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
-    "NameSeasonUnknown": "Nezināma Sezona",
-    "NameSeasonNumber": "Sezona {0}",
+    "NameSeasonUnknown": "Nezināma sezona",
+    "NameSeasonNumber": "{0}. sezona",
     "NameInstallFailed": "{0} instalācija neizdevās",
     "NameInstallFailed": "{0} instalācija neizdevās",
     "MusicVideos": "Mūzikas video",
     "MusicVideos": "Mūzikas video",
     "Music": "Mūzika",
     "Music": "Mūzika",
     "Movies": "Filmas",
     "Movies": "Filmas",
     "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
     "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
     "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
     "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
     "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
     "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
     "Latest": "Jaunākais",
     "Latest": "Jaunākais",
@@ -71,57 +71,57 @@
     "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
     "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
     "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
     "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
     "HeaderLiveTV": "Tiešraides TV",
     "HeaderLiveTV": "Tiešraides TV",
-    "HeaderContinueWatching": "Turpināt Skatīšanos",
-    "HeaderAlbumArtists": "Albumu Izpildītāji",
+    "HeaderContinueWatching": "Turpināt skatīšanos",
+    "HeaderAlbumArtists": "Albumu izpildītāji",
     "Genres": "Žanri",
     "Genres": "Žanri",
     "Folders": "Mapes",
     "Folders": "Mapes",
-    "Favorites": "Favorīti",
-    "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
-    "DeviceOnlineWithName": "{0} ir pievienojies",
-    "DeviceOfflineWithName": "{0} ir atvienojies",
+    "Favorites": "Izlase",
+    "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
+    "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
+    "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
     "Collections": "Kolekcijas",
     "Collections": "Kolekcijas",
     "Channels": "Kanāli",
     "Channels": "Kanāli",
-    "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}",
+    "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
     "Books": "Grāmatas",
     "Books": "Grāmatas",
     "Artists": "Izpildītāji",
     "Artists": "Izpildītāji",
     "Albums": "Albumi",
     "Albums": "Albumi",
     "ProviderValue": "Provider: {0}",
     "ProviderValue": "Provider: {0}",
-    "HeaderFavoriteSongs": "Dziesmu Favorīti",
-    "HeaderFavoriteShows": "Raidījumu Favorīti",
-    "HeaderFavoriteEpisodes": "Episožu Favorīti",
-    "HeaderFavoriteArtists": "Izpildītāju Favorīti",
-    "HeaderFavoriteAlbums": "Albumu Favorīti",
-    "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
-    "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
+    "HeaderFavoriteSongs": "Dziesmu izlase",
+    "HeaderFavoriteShows": "Raidījumu izlase",
+    "HeaderFavoriteEpisodes": "Sēriju izlase",
+    "HeaderFavoriteArtists": "Izpildītāju izlase",
+    "HeaderFavoriteAlbums": "Albumu izlase",
+    "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
+    "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
     "TasksApplicationCategory": "Lietotne",
     "TasksApplicationCategory": "Lietotne",
     "TasksLibraryCategory": "Bibliotēka",
     "TasksLibraryCategory": "Bibliotēka",
     "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
     "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
-    "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus",
+    "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
     "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
     "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
-    "TaskRefreshChannels": "Atjaunot Kanālus",
-    "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
-    "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
+    "TaskRefreshChannels": "Atjaunot kanālus",
+    "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
+    "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
     "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
     "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
-    "TaskUpdatePlugins": "Atjaunot Paplašinājumus",
+    "TaskUpdatePlugins": "Atjaunot paplašinājumus",
     "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
     "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
-    "TaskRefreshPeople": "Atjaunot Cilvēkus",
-    "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
-    "TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
+    "TaskRefreshPeople": "Atjaunot cilvēkus",
+    "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
+    "TaskCleanLogs": "Iztīrīt logdatņu mapi",
     "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
     "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
-    "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku",
+    "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
     "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
     "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
-    "TaskCleanCache": "Iztīrīt Kešošanas Mapi",
-    "TasksChannelsCategory": "Interneta Kanāli",
+    "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
+    "TasksChannelsCategory": "Interneta kanāli",
     "TasksMaintenanceCategory": "Apkope",
     "TasksMaintenanceCategory": "Apkope",
-    "Forced": "Piespiests",
+    "Forced": "Piespiedu",
     "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
     "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
-    "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu",
+    "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
     "Undefined": "Nenoteikts",
     "Undefined": "Nenoteikts",
     "Default": "Noklusējuma",
     "Default": "Noklusējuma",
-    "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
+    "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
     "TaskOptimizeDatabase": "Optimizēt datubāzi",
     "TaskOptimizeDatabase": "Optimizēt datubāzi",
     "External": "Ārējais",
     "External": "Ārējais",
     "HearingImpaired": "Ar dzirdes traucējumiem",
     "HearingImpaired": "Ar dzirdes traucējumiem",
-    "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors",
+    "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
     "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
     "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
 }
 }

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

@@ -121,5 +121,7 @@
     "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
     "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
     "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
     "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
     "HearingImpaired": "കേൾവി തകരാറുകൾ",
     "HearingImpaired": "കേൾവി തകരാറുകൾ",
-    "External": "പുറമേയുള്ള"
+    "External": "പുറമേയുള്ള",
+    "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+    "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
 }
 }

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

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

+ 1 - 1
Emby.Server.Implementations/Localization/Core/sk.json

@@ -124,5 +124,5 @@
     "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
     "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
     "TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
     "TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
     "External": "Externé",
     "External": "Externé",
-    "HearingImpaired": "Sluchovo Postihnutý"
+    "HearingImpaired": "Sluchovo postihnutí"
 }
 }

+ 20 - 17
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
                 string countryCode = resource.Substring(RatingsPath.Length, 2);
                 string countryCode = resource.Substring(RatingsPath.Length, 2);
                 var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
                 var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
 
 
-                await using var stream = _assembly.GetManifestResourceStream(resource);
-                using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
-                await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
+                var stream = _assembly.GetManifestResourceStream(resource);
+                await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
                 {
                 {
-                    if (string.IsNullOrWhiteSpace(line))
+                    using var reader = new StreamReader(stream!);
+                    await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
                     {
                     {
-                        continue;
-                    }
-
-                    string[] parts = line.Split(',');
-                    if (parts.Length == 2
-                        && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
-                    {
-                        var name = parts[0];
-                        dict.Add(name, new ParentalRating(name, value));
-                    }
-                    else
-                    {
-                        _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+                        if (string.IsNullOrWhiteSpace(line))
+                        {
+                            continue;
+                        }
+
+                        string[] parts = line.Split(',');
+                        if (parts.Length == 2
+                            && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+                        {
+                            var name = parts[0];
+                            dict.Add(name, new ParentalRating(name, value));
+                        }
+                        else
+                        {
+                            _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
+                        }
                     }
                     }
                 }
                 }
 
 

+ 1 - 1
Emby.Server.Implementations/MediaEncoder/EncodingManager.cs

@@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
         {
         {
             var deadImages = images
             var deadImages = images
                 .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
                 .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
-                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
                 .ToList();
                 .ToList();
 
 
             foreach (var image in deadImages)
             foreach (var image in deadImages)

+ 26 - 13
Emby.Server.Implementations/Net/SocketFactory.cs

@@ -1,12 +1,15 @@
-#pragma warning disable CS1591
-
 using System;
 using System;
+using System.Linq;
 using System.Net;
 using System.Net;
+using System.Net.NetworkInformation;
 using System.Net.Sockets;
 using System.Net.Sockets;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 
 
 namespace Emby.Server.Implementations.Net
 namespace Emby.Server.Implementations.Net
 {
 {
+    /// <summary>
+    /// Factory class to create different kinds of sockets.
+    /// </summary>
     public class SocketFactory : ISocketFactory
     public class SocketFactory : ISocketFactory
     {
     {
         /// <inheritdoc />
         /// <inheritdoc />
@@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
             }
             }
             catch
             catch
             {
             {
-                socket?.Dispose();
+                socket.Dispose();
 
 
                 throw;
                 throw;
             }
             }
@@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
         /// <inheritdoc />
         /// <inheritdoc />
         public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
         public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
         {
         {
-            ArgumentNullException.ThrowIfNull(bindInterface.Address);
+            var interfaceAddress = bindInterface.Address;
+            ArgumentNullException.ThrowIfNull(interfaceAddress);
 
 
             if (localPort < 0)
             if (localPort < 0)
             {
             {
@@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
             try
             try
             {
             {
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
-                socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
+                socket.Bind(new IPEndPoint(interfaceAddress, localPort));
 
 
                 return socket;
                 return socket;
             }
             }
             catch
             catch
             {
             {
-                socket?.Dispose();
+                socket.Dispose();
 
 
                 throw;
                 throw;
             }
             }
@@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
 
 
             try
             try
             {
             {
-                var interfaceIndex = bindInterface.Index;
-                var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
-
                 socket.MulticastLoopback = false;
                 socket.MulticastLoopback = false;
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
-                socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
-                socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
-                socket.Bind(new IPEndPoint(multicastAddress, localPort));
+
+                if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+                {
+                    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+                    socket.Bind(new IPEndPoint(multicastAddress, localPort));
+                }
+                else
+                {
+                    // Only create socket if interface supports multicast
+                    var interfaceIndex = bindInterface.Index;
+                    var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
+
+                    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+                    socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+                }
 
 
                 return socket;
                 return socket;
             }
             }
             catch
             catch
             {
             {
-                socket?.Dispose();
+                socket.Dispose();
 
 
                 throw;
                 throw;
             }
             }

+ 6 - 10
Emby.Server.Implementations/Playlists/PlaylistManager.cs

@@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
             // this is probably best done as a metadata provider
             // this is probably best done as a metadata provider
             // saving a file over itself will require some work to prevent this from happening when not needed
             // saving a file over itself will require some work to prevent this from happening when not needed
             var playlistPath = item.Path;
             var playlistPath = item.Path;
-            var extension = Path.GetExtension(playlistPath);
+            var extension = Path.GetExtension(playlistPath.AsSpan());
 
 
-            if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
+            if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new WplPlaylist();
                 var playlist = new WplPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())
@@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new WplContent().ToText(playlist);
                 string text = new WplContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new ZplPlaylist();
                 var playlist = new ZplPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())
@@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new ZplContent().ToText(playlist);
                 string text = new ZplContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new M3uPlaylist
                 var playlist = new M3uPlaylist
                 {
                 {
@@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new M3uContent().ToText(playlist);
                 string text = new M3uContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new M3uPlaylist();
                 var playlist = new M3uPlaylist();
                 playlist.IsExtended = true;
                 playlist.IsExtended = true;
@@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
                 string text = new M3uContent().ToText(playlist);
                 string text = new M3uContent().ToText(playlist);
                 File.WriteAllText(playlistPath, text);
                 File.WriteAllText(playlistPath, text);
             }
             }
-
-            if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
+            else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
             {
             {
                 var playlist = new PlsPlaylist();
                 var playlist = new PlsPlaylist();
                 foreach (var child in item.GetLinkedChildren())
                 foreach (var child in item.GetLinkedChildren())

+ 13 - 5
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
                 var url = new Uri(packageInfo.ImageUrl);
                 var url = new Uri(packageInfo.ImageUrl);
                 imagePath = Path.Join(path, url.Segments[^1]);
                 imagePath = Path.Join(path, url.Segments[^1]);
 
 
-                await using var fileStream = AsyncFile.OpenWrite(imagePath);
-
+                var fileStream = AsyncFile.OpenWrite(imagePath);
+                Stream? downloadStream = null;
                 try
                 try
                 {
                 {
-                    await using var downloadStream = await HttpClientFactory
+                    downloadStream = await HttpClientFactory
                         .CreateClient(NamedClient.Default)
                         .CreateClient(NamedClient.Default)
                         .GetStreamAsync(url)
                         .GetStreamAsync(url)
                         .ConfigureAwait(false);
                         .ConfigureAwait(false);
@@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
                     _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
                     _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
                     imagePath = string.Empty;
                     imagePath = string.Empty;
                 }
                 }
+                finally
+                {
+                    await fileStream.DisposeAsync().ConfigureAwait(false);
+                    if (downloadStream is not null)
+                    {
+                        await downloadStream.DisposeAsync().ConfigureAwait(false);
+                    }
+                }
             }
             }
 
 
             var manifest = new PluginManifest
             var manifest = new PluginManifest
@@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
                 ImagePath = imagePath
                 ImagePath = imagePath
             };
             };
 
 
-            if (!await ReconcileManifest(manifest, path))
+            if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
             {
             {
                 // An error occurred during reconciliation and saving could be undesirable.
                 // An error occurred during reconciliation and saving could be undesirable.
                 return false;
                 return false;
@@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
                 }
                 }
 
 
                 using var metaStream = File.OpenRead(metafile);
                 using var metaStream = File.OpenRead(metafile);
-                var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
+                var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false);
                 localManifest ??= new PluginManifest();
                 localManifest ??= new PluginManifest();
 
 
                 if (!Equals(localManifest.Id, manifest.Id))
                 if (!Equals(localManifest.Id, manifest.Id))

+ 2 - 2
Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs

@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             {
             {
                 try
                 try
                 {
                 {
-                    previouslyFailedImages = File.ReadAllText(failHistoryPath)
+                    previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
                         .Split('|', StringSplitOptions.RemoveEmptyEntries)
                         .Split('|', StringSplitOptions.RemoveEmptyEntries)
                         .ToList();
                         .ToList();
                 }
                 }
@@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                         }
                         }
 
 
                         string text = string.Join('|', previouslyFailedImages);
                         string text = string.Join('|', previouslyFailedImages);
-                        File.WriteAllText(failHistoryPath, text);
+                        await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
                     }
                     }
 
 
                     numComplete++;
                     numComplete++;

+ 104 - 0
Emby.Server.Implementations/SystemManager.cs

@@ -0,0 +1,104 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Updates;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Hosting;
+
+namespace Emby.Server.Implementations;
+
+/// <inheritdoc />
+public class SystemManager : ISystemManager
+{
+    private readonly IHostApplicationLifetime _applicationLifetime;
+    private readonly IServerApplicationHost _applicationHost;
+    private readonly IServerApplicationPaths _applicationPaths;
+    private readonly IServerConfigurationManager _configurationManager;
+    private readonly IStartupOptions _startupOptions;
+    private readonly IInstallationManager _installationManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="SystemManager"/> class.
+    /// </summary>
+    /// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
+    /// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
+    /// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
+    /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
+    /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
+    /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
+    public SystemManager(
+        IHostApplicationLifetime applicationLifetime,
+        IServerApplicationHost applicationHost,
+        IServerApplicationPaths applicationPaths,
+        IServerConfigurationManager configurationManager,
+        IStartupOptions startupOptions,
+        IInstallationManager installationManager)
+    {
+        _applicationLifetime = applicationLifetime;
+        _applicationHost = applicationHost;
+        _applicationPaths = applicationPaths;
+        _configurationManager = configurationManager;
+        _startupOptions = startupOptions;
+        _installationManager = installationManager;
+    }
+
+    /// <inheritdoc />
+    public SystemInfo GetSystemInfo(HttpRequest request)
+    {
+        return new SystemInfo
+        {
+            HasPendingRestart = _applicationHost.HasPendingRestart,
+            IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
+            Version = _applicationHost.ApplicationVersionString,
+            WebSocketPortNumber = _applicationHost.HttpPort,
+            CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
+            Id = _applicationHost.SystemId,
+            ProgramDataPath = _applicationPaths.ProgramDataPath,
+            WebPath = _applicationPaths.WebPath,
+            LogPath = _applicationPaths.LogDirectoryPath,
+            ItemsByNamePath = _applicationPaths.InternalMetadataPath,
+            InternalMetadataPath = _applicationPaths.InternalMetadataPath,
+            CachePath = _applicationPaths.CachePath,
+            TranscodingTempPath = _configurationManager.GetTranscodePath(),
+            ServerName = _applicationHost.FriendlyName,
+            LocalAddress = _applicationHost.GetSmartApiUrl(request),
+            SupportsLibraryMonitor = true,
+            PackageName = _startupOptions.PackageName,
+            CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
+        };
+    }
+
+    /// <inheritdoc />
+    public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
+    {
+        return new PublicSystemInfo
+        {
+            Version = _applicationHost.ApplicationVersionString,
+            ProductName = _applicationHost.Name,
+            Id = _applicationHost.SystemId,
+            ServerName = _applicationHost.FriendlyName,
+            LocalAddress = _applicationHost.GetSmartApiUrl(request),
+            StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
+        };
+    }
+
+    /// <inheritdoc />
+    public void Restart() => ShutdownInternal(true);
+
+    /// <inheritdoc />
+    public void Shutdown() => ShutdownInternal(false);
+
+    private void ShutdownInternal(bool restart)
+    {
+        Task.Run(async () =>
+        {
+            await Task.Delay(100).ConfigureAwait(false);
+            _applicationHost.ShouldRestart = restart;
+            _applicationLifetime.StopApplication();
+        });
+    }
+}

+ 13 - 9
Emby.Server.Implementations/Udp/UdpServer.cs

@@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
 
 
         private readonly byte[] _receiveBuffer = new byte[8192];
         private readonly byte[] _receiveBuffer = new byte[8192];
 
 
-        private Socket _udpSocket;
-        private IPEndPoint _endpoint;
-        private bool _disposed = false;
+        private readonly Socket _udpSocket;
+        private readonly IPEndPoint _endpoint;
+        private bool _disposed;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServer" /> class.
         /// Initializes a new instance of the <see cref="UdpServer" /> class.
@@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
 
 
             _endpoint = new IPEndPoint(bindAddress, port);
             _endpoint = new IPEndPoint(bindAddress, port);
 
 
-            _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
+            _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
+            {
+                MulticastLoopback = false,
+            };
             _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
             _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
         }
         }
 
 
@@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
 
 
             try
             try
             {
             {
+                _logger.LogDebug("Sending AutoDiscovery response");
                 await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
                 await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
             }
             }
             catch (SocketException ex)
             catch (SocketException ex)
@@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
             {
             {
                 try
                 try
                 {
                 {
-                    var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
+                    var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
+                    var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
                     var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
                     var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
                     if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
                     if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
                     {
                     {
@@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
                 }
                 }
                 catch (OperationCanceledException)
                 catch (OperationCanceledException)
                 {
                 {
-                    // Don't throw
+                    _logger.LogDebug("Broadcast socket operation cancelled");
                 }
                 }
             }
             }
         }
         }
@@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
                 return;
                 return;
             }
             }
 
 
-            _udpSocket?.Dispose();
-
-            GC.SuppressFinalize(this);
+            _udpSocket.Dispose();
+            _disposed = true;
         }
         }
     }
     }
 }
 }

+ 3 - 5
Emby.Server.Implementations/Updates/InstallationManager.cs

@@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
 
 
         private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
         private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
         {
         {
-            var extension = Path.GetExtension(package.SourceUrl);
-            if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
+            if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
             {
             {
                 _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
                 _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
                 return;
                 return;
@@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
 
 
             // CA5351: Do Not Use Broken Cryptographic Algorithms
             // CA5351: Do Not Use Broken Cryptographic Algorithms
 #pragma warning disable CA5351
 #pragma warning disable CA5351
-            using var md5 = MD5.Create();
             cancellationToken.ThrowIfCancellationRequested();
             cancellationToken.ThrowIfCancellationRequested();
 
 
-            var hash = Convert.ToHexString(md5.ComputeHash(stream));
+            var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
             if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
             if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
             {
             {
                 _logger.LogError(
                 _logger.LogError(
@@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
             reader.ExtractToDirectory(targetDir, true);
             reader.ExtractToDirectory(targetDir, true);
 
 
             // Ensure we create one or populate existing ones with missing data.
             // Ensure we create one or populate existing ones with missing data.
-            await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status);
+            await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
 
 
             _pluginManager.ImportPluginFrom(targetDir);
             _pluginManager.ImportPluginFrom(targetDir);
         }
         }

+ 11 - 5
Jellyfin.Api/Controllers/DlnaServerController.cs

@@ -5,7 +5,6 @@ using System.IO;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Dlna;
 using Emby.Dlna;
-using Emby.Dlna.Main;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Constants;
 using Jellyfin.Api.Constants;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
@@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
     /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
     /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
     /// </summary>
     /// </summary>
     /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
     /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
-    public DlnaServerController(IDlnaManager dlnaManager)
+    /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param>
+    /// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param>
+    /// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param>
+    public DlnaServerController(
+        IDlnaManager dlnaManager,
+        IContentDirectory contentDirectory,
+        IConnectionManager connectionManager,
+        IMediaReceiverRegistrar mediaReceiverRegistrar)
     {
     {
         _dlnaManager = dlnaManager;
         _dlnaManager = dlnaManager;
-        _contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
-        _connectionManager = DlnaEntryPoint.Current.ConnectionManager;
-        _mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
+        _contentDirectory = contentDirectory;
+        _connectionManager = connectionManager;
+        _mediaReceiverRegistrar = mediaReceiverRegistrar;
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 28 - 20
Jellyfin.Api/Controllers/DynamicHlsController.cs

@@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
     private const string DefaultEventEncoderPreset = "superfast";
     private const string DefaultEventEncoderPreset = "superfast";
     private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
     private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
 
 
+    private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
+
     private readonly ILibraryManager _libraryManager;
     private readonly ILibraryManager _libraryManager;
     private readonly IUserManager _userManager;
     private readonly IUserManager _userManager;
     private readonly IDlnaManager _dlnaManager;
     private readonly IDlnaManager _dlnaManager;
@@ -408,6 +410,7 @@ public class DynamicHlsController : BaseJellyfinApiController
     /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
     /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
     /// <param name="streamOptions">Optional. The streaming options.</param>
     /// <param name="streamOptions">Optional. The streaming options.</param>
     /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
     /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
+    /// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
     /// <response code="200">Video stream returned.</response>
     /// <response code="200">Video stream returned.</response>
     /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
     /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
     [HttpGet("Videos/{itemId}/master.m3u8")]
     [HttpGet("Videos/{itemId}/master.m3u8")]
@@ -465,7 +468,8 @@ public class DynamicHlsController : BaseJellyfinApiController
         [FromQuery] int? videoStreamIndex,
         [FromQuery] int? videoStreamIndex,
         [FromQuery] EncodingContext? context,
         [FromQuery] EncodingContext? context,
         [FromQuery] Dictionary<string, string> streamOptions,
         [FromQuery] Dictionary<string, string> streamOptions,
-        [FromQuery] bool enableAdaptiveBitrateStreaming = true)
+        [FromQuery] bool enableAdaptiveBitrateStreaming = true,
+        [FromQuery] bool enableTrickplay = true)
     {
     {
         var streamingRequest = new HlsVideoRequestDto
         var streamingRequest = new HlsVideoRequestDto
         {
         {
@@ -519,7 +523,8 @@ public class DynamicHlsController : BaseJellyfinApiController
             VideoStreamIndex = videoStreamIndex,
             VideoStreamIndex = videoStreamIndex,
             Context = context ?? EncodingContext.Streaming,
             Context = context ?? EncodingContext.Streaming,
             StreamOptions = streamOptions,
             StreamOptions = streamOptions,
-            EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
+            EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
+            EnableTrickplay = enableTrickplay
         };
         };
 
 
         return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
         return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@@ -1705,16 +1710,31 @@ public class DynamicHlsController : BaseJellyfinApiController
         var audioCodec = _encodingHelper.GetAudioEncoder(state);
         var audioCodec = _encodingHelper.GetAudioEncoder(state);
         var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
         var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
 
 
+        // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
+        var strictArgs = string.Empty;
+        var actualOutputAudioCodec = state.ActualOutputAudioCodec;
+        if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
+            || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
+            || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
+            || (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
+                && _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
+        {
+            strictArgs = " -strict -2";
+        }
+
         if (!state.IsOutputVideo)
         if (!state.IsOutputVideo)
         {
         {
+            var audioTranscodeParams = string.Empty;
+
+            // -vn to drop any video streams
+            audioTranscodeParams += "-vn";
+
             if (EncodingHelper.IsCopyCodec(audioCodec))
             if (EncodingHelper.IsCopyCodec(audioCodec))
             {
             {
-                return "-acodec copy -strict -2" + bitStreamArgs;
+                return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
             }
             }
 
 
-            var audioTranscodeParams = string.Empty;
-
-            audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
+            audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
 
 
             var audioBitrate = state.OutputAudioBitrate;
             var audioBitrate = state.OutputAudioBitrate;
             var audioChannels = state.OutputAudioChannels;
             var audioChannels = state.OutputAudioChannels;
@@ -1742,21 +1762,9 @@ public class DynamicHlsController : BaseJellyfinApiController
                 audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
                 audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
             }
             }
 
 
-            audioTranscodeParams += " -vn";
             return audioTranscodeParams;
             return audioTranscodeParams;
         }
         }
 
 
-        // dts, flac, opus and truehd are experimental in mp4 muxer
-        var strictArgs = string.Empty;
-        var actualOutputAudioCodec = state.ActualOutputAudioCodec;
-        if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
-            || string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
-        {
-            strictArgs = " -strict -2";
-        }
-
         if (EncodingHelper.IsCopyCodec(audioCodec))
         if (EncodingHelper.IsCopyCodec(audioCodec))
         {
         {
             var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
             var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
@@ -2041,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
             return null;
             return null;
         }
         }
 
 
-        var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
+        var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
 
 
-        var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
+        var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
 
 
         return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
         return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
     }
     }

+ 5 - 4
Jellyfin.Api/Controllers/HlsSegmentController.cs

@@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
     public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
     public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
     {
     {
         // TODO: Deprecate with new iOS app
         // TODO: Deprecate with new iOS app
-        var file = segmentId + Path.GetExtension(Request.Path);
+        var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         var fileDir = Path.GetDirectoryName(file);
         var fileDir = Path.GetDirectoryName(file);
@@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
     [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
     public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
     public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
     {
     {
-        var file = playlistId + Path.GetExtension(Request.Path);
+        var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         var transcodePath = _serverConfigurationManager.GetTranscodePath();
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         file = Path.GetFullPath(Path.Combine(transcodePath, file));
         var fileDir = Path.GetDirectoryName(file);
         var fileDir = Path.GetDirectoryName(file);
-        if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
+        if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
+            || Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
         {
         {
             return BadRequest("Invalid segment.");
             return BadRequest("Invalid segment.");
         }
         }
@@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
         [FromRoute, Required] string segmentId,
         [FromRoute, Required] string segmentId,
         [FromRoute, Required] string segmentContainer)
         [FromRoute, Required] string segmentContainer)
     {
     {
-        var file = segmentId + Path.GetExtension(Request.Path);
+        var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
         var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
         var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
 
 
         file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
         file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));

+ 19 - 24
Jellyfin.Api/Controllers/ImageController.cs

@@ -7,6 +7,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
+using System.Security.Cryptography;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Attributes;
 using Jellyfin.Api.Attributes;
@@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
         _appPaths = appPaths;
         _appPaths = appPaths;
     }
     }
 
 
+    private static Stream GetFromBase64Stream(Stream inputStream)
+        => new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
+
     /// <summary>
     /// <summary>
     /// Sets the user image.
     /// Sets the user image.
     /// </summary>
     /// </summary>
@@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 
 
             await _providerManager
             await _providerManager
-                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+                .SaveImage(stream, mimeType, user.ProfileImage.Path)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
 
@@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
             user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
 
 
             await _providerManager
             await _providerManager
-                .SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
+                .SaveImage(stream, mimeType, user.ProfileImage.Path)
                 .ConfigureAwait(false);
                 .ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
             await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
 
 
@@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
-            await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 
 
             return NoContent();
             return NoContent();
@@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             // Handle image/png; charset=utf-8
             // Handle image/png; charset=utf-8
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
             var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
-            await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
+            await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
             await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
 
 
             return NoContent();
             return NoContent();
@@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
             return BadRequest("Incorrect ContentType.");
             return BadRequest("Incorrect ContentType.");
         }
         }
 
 
-        var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = GetFromBase64Stream(Request.Body);
+        await using (stream.ConfigureAwait(false))
         {
         {
             var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
             var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
             var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
             var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
@@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
             var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
             var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
             await using (fs.ConfigureAwait(false))
             await using (fs.ConfigureAwait(false))
             {
             {
-                await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
+                await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
             }
             }
 
 
             return NoContent();
             return NoContent();
@@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
         return NoContent();
         return NoContent();
     }
     }
 
 
-    private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
-    {
-        using var reader = new StreamReader(inputStream);
-        var text = await reader.ReadToEndAsync().ConfigureAwait(false);
-
-        var bytes = Convert.FromBase64String(text);
-        return new MemoryStream(bytes, 0, bytes.Length, false, true);
-    }
-
     private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
     private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
     {
     {
         int? width = null;
         int? width = null;

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

@@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
 
 
         return new AllThemeMediaResult
         return new AllThemeMediaResult
         {
         {
-            ThemeSongsResult = themeSongs?.Value,
-            ThemeVideosResult = themeVideos?.Value,
+            ThemeSongsResult = themeSongs.Value,
+            ThemeVideosResult = themeVideos.Value,
             SoundtrackSongsResult = new ThemeMediaResult()
             SoundtrackSongsResult = new ThemeMediaResult()
         };
         };
     }
     }
@@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
 
 
             baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
             baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
 
 
-            parent = parent?.GetParent();
+            parent = parent.GetParent();
         }
         }
 
 
         return baseItemDtos;
         return baseItemDtos;

+ 4 - 4
Jellyfin.Api/Controllers/SubtitleController.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net.Mime;
 using System.Net.Mime;
+using System.Security.Cryptography;
 using System.Text;
 using System.Text;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
         [FromBody, Required] UploadSubtitleDto body)
         [FromBody, Required] UploadSubtitleDto body)
     {
     {
         var video = (Video)_libraryManager.GetItemById(itemId);
         var video = (Video)_libraryManager.GetItemById(itemId);
-        var data = Convert.FromBase64String(body.Data);
-        var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
-        await using (memoryStream.ConfigureAwait(false))
+        var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
+        await using (stream.ConfigureAwait(false))
         {
         {
             await _subtitleManager.UploadSubtitle(
             await _subtitleManager.UploadSubtitle(
                 video,
                 video,
@@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
                     Language = body.Language,
                     Language = body.Language,
                     IsForced = body.IsForced,
                     IsForced = body.IsForced,
                     IsHearingImpaired = body.IsHearingImpaired,
                     IsHearingImpaired = body.IsHearingImpaired,
-                    Stream = memoryStream
+                    Stream = stream
                 }).ConfigureAwait(false);
                 }).ConfigureAwait(false);
             _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
             _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
 
 

+ 22 - 25
Jellyfin.Api/Controllers/SystemController.cs

@@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
@@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
 /// </summary>
 /// </summary>
 public class SystemController : BaseJellyfinApiController
 public class SystemController : BaseJellyfinApiController
 {
 {
+    private readonly ILogger<SystemController> _logger;
     private readonly IServerApplicationHost _appHost;
     private readonly IServerApplicationHost _appHost;
     private readonly IApplicationPaths _appPaths;
     private readonly IApplicationPaths _appPaths;
     private readonly IFileSystem _fileSystem;
     private readonly IFileSystem _fileSystem;
-    private readonly INetworkManager _network;
-    private readonly ILogger<SystemController> _logger;
+    private readonly INetworkManager _networkManager;
+    private readonly ISystemManager _systemManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="SystemController"/> class.
     /// Initializes a new instance of the <see cref="SystemController"/> class.
     /// </summary>
     /// </summary>
-    /// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
+    /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+    /// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
     /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
     /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
     /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
-    /// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
-    /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
+    /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
+    /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
     public SystemController(
     public SystemController(
-        IServerConfigurationManager serverConfigurationManager,
+        ILogger<SystemController> logger,
         IServerApplicationHost appHost,
         IServerApplicationHost appHost,
+        IServerApplicationPaths appPaths,
         IFileSystem fileSystem,
         IFileSystem fileSystem,
-        INetworkManager network,
-        ILogger<SystemController> logger)
+        INetworkManager networkManager,
+        ISystemManager systemManager)
     {
     {
-        _appPaths = serverConfigurationManager.ApplicationPaths;
+        _logger = logger;
         _appHost = appHost;
         _appHost = appHost;
+        _appPaths = appPaths;
         _fileSystem = fileSystem;
         _fileSystem = fileSystem;
-        _network = network;
-        _logger = logger;
+        _networkManager = networkManager;
+        _systemManager = systemManager;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult<SystemInfo> GetSystemInfo()
     public ActionResult<SystemInfo> GetSystemInfo()
-    {
-        return _appHost.GetSystemInfo(Request);
-    }
+        => _systemManager.GetSystemInfo(Request);
 
 
     /// <summary>
     /// <summary>
     /// Gets public information about the server.
     /// Gets public information about the server.
@@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
     [HttpGet("Info/Public")]
     [HttpGet("Info/Public")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
     public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
-    {
-        return _appHost.GetPublicSystemInfo(Request);
-    }
+        => _systemManager.GetPublicSystemInfo(Request);
 
 
     /// <summary>
     /// <summary>
     /// Pings the system.
     /// Pings the system.
@@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
     [HttpPost("Ping", Name = "PostPingSystem")]
     [HttpPost("Ping", Name = "PostPingSystem")]
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<string> PingSystem()
     public ActionResult<string> PingSystem()
-    {
-        return _appHost.Name;
-    }
+        => _appHost.Name;
 
 
     /// <summary>
     /// <summary>
     /// Restarts the application.
     /// Restarts the application.
@@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult RestartApplication()
     public ActionResult RestartApplication()
     {
     {
-        _appHost.Restart();
+        _systemManager.Restart();
         return NoContent();
         return NoContent();
     }
     }
 
 
@@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     [ProducesResponseType(StatusCodes.Status403Forbidden)]
     public ActionResult ShutdownApplication()
     public ActionResult ShutdownApplication()
     {
     {
-        _appHost.Shutdown();
+        _systemManager.Shutdown();
         return NoContent();
         return NoContent();
     }
     }
 
 
@@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
         return new EndPointInfo
         return new EndPointInfo
         {
         {
             IsLocal = HttpContext.IsLocal(),
             IsLocal = HttpContext.IsLocal(),
-            IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
+            IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
         };
         };
     }
     }
 
 
@@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
     [ProducesResponseType(StatusCodes.Status200OK)]
     [ProducesResponseType(StatusCodes.Status200OK)]
     public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
     public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
     {
     {
-        var result = _network.GetMacAddresses()
+        var result = _networkManager.GetMacAddresses()
             .Select(i => new WakeOnLanInfo(i));
             .Select(i => new WakeOnLanInfo(i));
         return Ok(result);
         return Ok(result);
     }
     }

+ 101 - 0
Jellyfin.Api/Controllers/TrickplayController.cs

@@ -0,0 +1,101 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Jellyfin.Api.Attributes;
+using Jellyfin.Api.Extensions;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Jellyfin.Api.Controllers;
+
+/// <summary>
+/// Trickplay controller.
+/// </summary>
+[Route("")]
+[Authorize]
+public class TrickplayController : BaseJellyfinApiController
+{
+    private readonly ILibraryManager _libraryManager;
+    private readonly ITrickplayManager _trickplayManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TrickplayController"/> class.
+    /// </summary>
+    /// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
+    /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
+    public TrickplayController(
+        ILibraryManager libraryManager,
+        ITrickplayManager trickplayManager)
+    {
+        _libraryManager = libraryManager;
+        _trickplayManager = trickplayManager;
+    }
+
+    /// <summary>
+    /// Gets an image tiles playlist for trickplay.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <param name="width">The width of a single tile.</param>
+    /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+    /// <response code="200">Tiles playlist returned.</response>
+    /// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns>
+    [HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
+    [ProducesPlaylistFile]
+    public async Task<ActionResult> GetTrickplayHlsPlaylist(
+        [FromRoute, Required] Guid itemId,
+        [FromRoute, Required] int width,
+        [FromQuery] Guid? mediaSourceId)
+    {
+        string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
+
+        if (string.IsNullOrEmpty(playlist))
+        {
+            return NotFound();
+        }
+
+        return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8);
+    }
+
+    /// <summary>
+    /// Gets a trickplay tile image.
+    /// </summary>
+    /// <param name="itemId">The item id.</param>
+    /// <param name="width">The width of a single tile.</param>
+    /// <param name="index">The index of the desired tile.</param>
+    /// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
+    /// <response code="200">Tile image returned.</response>
+    /// <response code="200">Tile image not found at specified index.</response>
+    /// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
+    [HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
+    [ProducesResponseType(StatusCodes.Status200OK)]
+    [ProducesResponseType(StatusCodes.Status404NotFound)]
+    [ProducesImageFile]
+    public ActionResult GetTrickplayTileImage(
+        [FromRoute, Required] Guid itemId,
+        [FromRoute, Required] int width,
+        [FromRoute, Required] int index,
+        [FromQuery] Guid? mediaSourceId)
+    {
+        var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
+        if (item is null)
+        {
+            return NotFound();
+        }
+
+        var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
+        if (System.IO.File.Exists(path))
+        {
+            return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
+        }
+
+        return NotFound();
+    }
+}

+ 50 - 35
Jellyfin.Api/Helpers/DynamicHlsHelper.cs

@@ -9,6 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Extensions;
 using Jellyfin.Api.Models.StreamingDtos;
 using Jellyfin.Api.Models.StreamingDtos;
+using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
@@ -46,6 +48,7 @@ public class DynamicHlsHelper
     private readonly ILogger<DynamicHlsHelper> _logger;
     private readonly ILogger<DynamicHlsHelper> _logger;
     private readonly IHttpContextAccessor _httpContextAccessor;
     private readonly IHttpContextAccessor _httpContextAccessor;
     private readonly EncodingHelper _encodingHelper;
     private readonly EncodingHelper _encodingHelper;
+    private readonly ITrickplayManager _trickplayManager;
 
 
     /// <summary>
     /// <summary>
     /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
     /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
@@ -62,6 +65,7 @@ public class DynamicHlsHelper
     /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
     /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
     /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
     /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
     /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
     /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
+    /// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
     public DynamicHlsHelper(
     public DynamicHlsHelper(
         ILibraryManager libraryManager,
         ILibraryManager libraryManager,
         IUserManager userManager,
         IUserManager userManager,
@@ -74,7 +78,8 @@ public class DynamicHlsHelper
         INetworkManager networkManager,
         INetworkManager networkManager,
         ILogger<DynamicHlsHelper> logger,
         ILogger<DynamicHlsHelper> logger,
         IHttpContextAccessor httpContextAccessor,
         IHttpContextAccessor httpContextAccessor,
-        EncodingHelper encodingHelper)
+        EncodingHelper encodingHelper,
+        ITrickplayManager trickplayManager)
     {
     {
         _libraryManager = libraryManager;
         _libraryManager = libraryManager;
         _userManager = userManager;
         _userManager = userManager;
@@ -88,6 +93,7 @@ public class DynamicHlsHelper
         _logger = logger;
         _logger = logger;
         _httpContextAccessor = httpContextAccessor;
         _httpContextAccessor = httpContextAccessor;
         _encodingHelper = encodingHelper;
         _encodingHelper = encodingHelper;
+        _trickplayManager = trickplayManager;
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -200,13 +206,6 @@ public class DynamicHlsHelper
 
 
         if (state.VideoStream is not null && state.VideoRequest is not null)
         if (state.VideoStream is not null && state.VideoRequest is not null)
         {
         {
-            // Provide a workaround for the case issue between flac and fLaC.
-            var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
-            if (!string.IsNullOrEmpty(flacWaPlaylist))
-            {
-                builder.Append(flacWaPlaylist);
-            }
-
             var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
             var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
 
 
             // Provide SDR HEVC entrance for backward compatibility.
             // Provide SDR HEVC entrance for backward compatibility.
@@ -236,14 +235,7 @@ public class DynamicHlsHelper
                     }
                     }
 
 
                     var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
                     var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
-                    var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
-
-                    // Provide a workaround for the case issue between flac and fLaC.
-                    flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
-                    if (!string.IsNullOrEmpty(flacWaPlaylist))
-                    {
-                        builder.Append(flacWaPlaylist);
-                    }
+                    AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
 
 
                     // Restore the video codec
                     // Restore the video codec
                     state.OutputVideoCodec = "copy";
                     state.OutputVideoCodec = "copy";
@@ -274,13 +266,6 @@ public class DynamicHlsHelper
                 state.VideoStream.Level = originalLevel;
                 state.VideoStream.Level = originalLevel;
                 var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
                 var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
                 builder.Append(newPlaylist);
                 builder.Append(newPlaylist);
-
-                // Provide a workaround for the case issue between flac and fLaC.
-                flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
-                if (!string.IsNullOrEmpty(flacWaPlaylist))
-                {
-                    builder.Append(flacWaPlaylist);
-                }
             }
             }
         }
         }
 
 
@@ -301,6 +286,13 @@ public class DynamicHlsHelper
             AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
             AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
         }
         }
 
 
+        if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
+        {
+            var sourceId = Guid.Parse(state.Request.MediaSourceId);
+            var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
+            AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
+        }
+
         return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
         return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
     }
     }
 
 
@@ -529,6 +521,41 @@ public class DynamicHlsHelper
         }
         }
     }
     }
 
 
+    /// <summary>
+    /// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
+    /// </summary>
+    /// <param name="state">StreamState of the current stream.</param>
+    /// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
+    /// <param name="builder">StringBuilder to append the field to.</param>
+    /// <param name="user">Http user context.</param>
+    private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
+    {
+        const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
+
+        foreach (var resolution in trickplayResolutions)
+        {
+            var width = resolution.Key;
+            var trickplayInfo = resolution.Value;
+
+            var url = string.Format(
+                CultureInfo.InvariantCulture,
+                "Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
+                width.ToString(CultureInfo.InvariantCulture),
+                state.Request.MediaSourceId,
+                user.GetToken());
+
+            builder.AppendFormat(
+                CultureInfo.InvariantCulture,
+                playlistFormat,
+                trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
+                trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
+                trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
+                url);
+
+            builder.AppendLine();
+        }
+    }
+
     /// <summary>
     /// <summary>
     /// Get the H.26X level of the output video stream.
     /// Get the H.26X level of the output video stream.
     /// </summary>
     /// </summary>
@@ -767,16 +794,4 @@ public class DynamicHlsHelper
             newValue.ToString(),
             newValue.ToString(),
             StringComparison.Ordinal);
             StringComparison.Ordinal);
     }
     }
-
-    private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
-    {
-        if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
-        {
-            return string.Empty;
-        }
-
-        var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
-
-        return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
-    }
 }
 }

+ 5 - 3
Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs

@@ -5,7 +5,9 @@ using System.Text;
 namespace Jellyfin.Api.Helpers;
 namespace Jellyfin.Api.Helpers;
 
 
 /// <summary>
 /// <summary>
-/// Hls Codec string helpers.
+/// Helpers to generate HLS codec strings according to
+/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
+/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
 /// </summary>
 /// </summary>
 public static class HlsCodecStringHelpers
 public static class HlsCodecStringHelpers
 {
 {
@@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
     /// <summary>
     /// <summary>
     /// Codec name for FLAC.
     /// Codec name for FLAC.
     /// </summary>
     /// </summary>
-    public const string FLAC = "flac";
+    public const string FLAC = "fLaC";
 
 
     /// <summary>
     /// <summary>
     /// Codec name for ALAC.
     /// Codec name for ALAC.
@@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
     /// <summary>
     /// <summary>
     /// Codec name for OPUS.
     /// Codec name for OPUS.
     /// </summary>
     /// </summary>
-    public const string OPUS = "opus";
+    public const string OPUS = "Opus";
 
 
     /// <summary>
     /// <summary>
     /// Gets a MP3 codec string.
     /// Gets a MP3 codec string.

+ 6 - 8
Jellyfin.Api/Helpers/StreamingHelpers.cs

@@ -248,7 +248,7 @@ public static class StreamingHelpers
             ? GetOutputFileExtension(state, mediaSource)
             ? GetOutputFileExtension(state, mediaSource)
             : ("." + state.OutputContainer);
             : ("." + state.OutputContainer);
 
 
-        state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
+        state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
 
 
         return state;
         return state;
     }
     }
@@ -421,10 +421,9 @@ public static class StreamingHelpers
     /// <param name="state">The state.</param>
     /// <param name="state">The state.</param>
     /// <param name="mediaSource">The mediaSource.</param>
     /// <param name="mediaSource">The mediaSource.</param>
     /// <returns>System.String.</returns>
     /// <returns>System.String.</returns>
-    private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
+    private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
     {
     {
         var ext = Path.GetExtension(state.RequestedUrl);
         var ext = Path.GetExtension(state.RequestedUrl);
-
         if (!string.IsNullOrEmpty(ext))
         if (!string.IsNullOrEmpty(ext))
         {
         {
             return ext;
             return ext;
@@ -463,10 +462,9 @@ public static class StreamingHelpers
                 return ".asf";
                 return ".asf";
             }
             }
         }
         }
-
-        // Try to infer based on the desired audio codec
-        if (!state.IsVideoRequest)
+        else
         {
         {
+            // Try to infer based on the desired audio codec
             var audioCodec = state.Request.AudioCodec;
             var audioCodec = state.Request.AudioCodec;
 
 
             if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
             if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
@@ -497,7 +495,7 @@ public static class StreamingHelpers
             return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
             return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
         }
         }
 
 
-        return null;
+        throw new InvalidOperationException("Failed to find an appropriate file extension");
     }
     }
 
 
     /// <summary>
     /// <summary>
@@ -514,7 +512,7 @@ public static class StreamingHelpers
         var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
         var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
 
 
         var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
         var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-        var ext = outputFileExtension?.ToLowerInvariant();
+        var ext = outputFileExtension.ToLowerInvariant();
         var folder = serverConfigurationManager.GetTranscodePath();
         var folder = serverConfigurationManager.GetTranscodePath();
 
 
         return Path.Combine(folder, filename + ext);
         return Path.Combine(folder, filename + ext);

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

@@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
                 await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
                 await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
             }
             }
 
 
-            if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
+            if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
             {
             {
                 string subtitlePath = state.SubtitleStream.Path;
                 string subtitlePath = state.SubtitleStream.Path;
                 string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
                 string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));

+ 0 - 2
Jellyfin.Api/Jellyfin.Api.csproj

@@ -8,8 +8,6 @@
   <PropertyGroup>
   <PropertyGroup>
     <TargetFramework>net7.0</TargetFramework>
     <TargetFramework>net7.0</TargetFramework>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
-    <NoWarn>AD0001</NoWarn>
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>

+ 10 - 10
Jellyfin.Api/Middleware/ExceptionMiddleware.cs

@@ -122,17 +122,17 @@ public class ExceptionMiddleware
 
 
     private static int GetStatusCode(Exception ex)
     private static int GetStatusCode(Exception ex)
     {
     {
-        switch (ex)
+        return ex switch
         {
         {
-            case ArgumentException _: return StatusCodes.Status400BadRequest;
-            case AuthenticationException _: return StatusCodes.Status401Unauthorized;
-            case SecurityException _: return StatusCodes.Status403Forbidden;
-            case DirectoryNotFoundException _:
-            case FileNotFoundException _:
-            case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
-            case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
-            default: return StatusCodes.Status500InternalServerError;
-        }
+            ArgumentException => StatusCodes.Status400BadRequest,
+            AuthenticationException => StatusCodes.Status401Unauthorized,
+            SecurityException => StatusCodes.Status403Forbidden,
+            DirectoryNotFoundException => StatusCodes.Status404NotFound,
+            FileNotFoundException => StatusCodes.Status404NotFound,
+            ResourceNotFoundException => StatusCodes.Status404NotFound,
+            MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
+            _ => StatusCodes.Status500InternalServerError
+        };
     }
     }
 
 
     private string NormalizeExceptionMessage(string msg)
     private string NormalizeExceptionMessage(string msg)

+ 6 - 1
Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs

@@ -1,4 +1,4 @@
-namespace Jellyfin.Api.Models.StreamingDtos;
+namespace Jellyfin.Api.Models.StreamingDtos;
 
 
 /// <summary>
 /// <summary>
 /// The video request dto.
 /// The video request dto.
@@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto
     /// Gets or sets a value indicating whether to enable subtitles in the manifest.
     /// Gets or sets a value indicating whether to enable subtitles in the manifest.
     /// </summary>
     /// </summary>
     public bool EnableSubtitlesInManifest { get; set; }
     public bool EnableSubtitlesInManifest { get; set; }
+
+    /// <summary>
+    /// Gets or sets a value indicating whether to enable trickplay images.
+    /// </summary>
+    public bool EnableTrickplay { get; set; }
 }
 }

+ 75 - 0
Jellyfin.Data/Entities/TrickplayInfo.cs

@@ -0,0 +1,75 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Jellyfin.Data.Entities;
+
+/// <summary>
+/// An entity representing the metadata for a group of trickplay tiles.
+/// </summary>
+public class TrickplayInfo
+{
+    /// <summary>
+    /// Gets or sets the id of the associated item.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    [JsonIgnore]
+    public Guid ItemId { get; set; }
+
+    /// <summary>
+    /// Gets or sets width of an individual thumbnail.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int Width { get; set; }
+
+    /// <summary>
+    /// Gets or sets height of an individual thumbnail.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int Height { get; set; }
+
+    /// <summary>
+    /// Gets or sets amount of thumbnails per row.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int TileWidth { get; set; }
+
+    /// <summary>
+    /// Gets or sets amount of thumbnails per column.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int TileHeight { get; set; }
+
+    /// <summary>
+    /// Gets or sets total amount of non-black thumbnails.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int ThumbnailCount { get; set; }
+
+    /// <summary>
+    /// Gets or sets interval in milliseconds between each trickplay thumbnail.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int Interval { get; set; }
+
+    /// <summary>
+    /// Gets or sets peak bandwith usage in bits per second.
+    /// </summary>
+    /// <remarks>
+    /// Required.
+    /// </remarks>
+    public int Bandwidth { get; set; }
+}

+ 6 - 0
Jellyfin.Data/Entities/User.cs

@@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
         /// </summary>
         /// </summary>
         public SyncPlayUserAccessType SyncPlayAccess { get; set; }
         public SyncPlayUserAccessType SyncPlayAccess { get; set; }
 
 
+        /// <summary>
+        /// Gets or sets the cast receiver id.
+        /// </summary>
+        [StringLength(32)]
+        public string? CastReceiverId { get; set; }
+
         /// <inheritdoc />
         /// <inheritdoc />
         [ConcurrencyCheck]
         [ConcurrencyCheck]
         public uint RowVersion { get; private set; }
         public uint RowVersion { get; private set; }

+ 9 - 7
Jellyfin.Networking/Extensions/NetworkExtensions.cs

@@ -1,7 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
-using System.Linq;
 using System.Net;
 using System.Net;
 using System.Net.Sockets;
 using System.Net.Sockets;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
@@ -204,7 +203,7 @@ public static partial class NetworkExtensions
         {
         {
             var ipBlock = splitString.Current;
             var ipBlock = splitString.Current;
             var address = IPAddress.None;
             var address = IPAddress.None;
-            if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
+            if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
             {
             {
                 address = tmpAddress;
                 address = tmpAddress;
             }
             }
@@ -231,12 +230,12 @@ public static partial class NetworkExtensions
                 }
                 }
                 else if (address.AddressFamily == AddressFamily.InterNetwork)
                 else if (address.AddressFamily == AddressFamily.InterNetwork)
                 {
                 {
-                    result = new IPNetwork(address, Network.MinimumIPv4PrefixSize);
+                    result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
                     return true;
                     return true;
                 }
                 }
                 else if (address.AddressFamily == AddressFamily.InterNetworkV6)
                 else if (address.AddressFamily == AddressFamily.InterNetworkV6)
                 {
                 {
-                    result = new IPNetwork(address, Network.MinimumIPv6PrefixSize);
+                    result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
                     return true;
                     return true;
                 }
                 }
             }
             }
@@ -284,12 +283,15 @@ public static partial class NetworkExtensions
 
 
         if (hosts.Count <= 2)
         if (hosts.Count <= 2)
         {
         {
+            var firstPart = hosts[0];
+
             // Is hostname or hostname:port
             // Is hostname or hostname:port
-            if (FqdnGeneratedRegex().IsMatch(hosts[0]))
+            if (FqdnGeneratedRegex().IsMatch(firstPart))
             {
             {
                 try
                 try
                 {
                 {
-                    addresses = Dns.GetHostAddresses(hosts[0]);
+                    // .NET automatically filters only supported returned addresses based on OS support.
+                    addresses = Dns.GetHostAddresses(firstPart);
                     return true;
                     return true;
                 }
                 }
                 catch (SocketException)
                 catch (SocketException)
@@ -299,7 +301,7 @@ public static partial class NetworkExtensions
             }
             }
 
 
             // Is an IPv4 or IPv4:port
             // Is an IPv4 or IPv4:port
-            if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address))
+            if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
             {
             {
                 if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
                 if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
                     || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
                     || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))

+ 157 - 87
Jellyfin.Networking/Manager/NetworkManager.cs

@@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.AspNetCore.HttpOverrides;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
+using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
 
 
 namespace Jellyfin.Networking.Manager
 namespace Jellyfin.Networking.Manager
 {
 {
@@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
 
 
         private readonly IConfigurationManager _configurationManager;
         private readonly IConfigurationManager _configurationManager;
 
 
+        private readonly IConfiguration _startupConfig;
+
         private readonly object _networkEventLock;
         private readonly object _networkEventLock;
 
 
         /// <summary>
         /// <summary>
         /// Holds the published server URLs and the IPs to use them on.
         /// Holds the published server URLs and the IPs to use them on.
         /// </summary>
         /// </summary>
-        private IReadOnlyDictionary<IPData, string> _publishedServerUrls;
+        private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
 
 
         private IReadOnlyList<IPNetwork> _remoteAddressFilter;
         private IReadOnlyList<IPNetwork> _remoteAddressFilter;
 
 
@@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="NetworkManager"/> class.
         /// Initializes a new instance of the <see cref="NetworkManager"/> class.
         /// </summary>
         /// </summary>
-        /// <param name="configurationManager">IServerConfigurationManager instance.</param>
+        /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
+        /// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
         /// <param name="logger">Logger to use for messages.</param>
         /// <param name="logger">Logger to use for messages.</param>
 #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
 #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
-        public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger)
+        public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
         {
         {
             ArgumentNullException.ThrowIfNull(logger);
             ArgumentNullException.ThrowIfNull(logger);
             ArgumentNullException.ThrowIfNull(configurationManager);
             ArgumentNullException.ThrowIfNull(configurationManager);
 
 
             _logger = logger;
             _logger = logger;
             _configurationManager = configurationManager;
             _configurationManager = configurationManager;
+            _startupConfig = startupConfig;
             _initLock = new();
             _initLock = new();
             _interfaces = new List<IPData>();
             _interfaces = new List<IPData>();
             _macAddresses = new List<PhysicalAddress>();
             _macAddresses = new List<PhysicalAddress>();
-            _publishedServerUrls = new Dictionary<IPData, string>();
+            _publishedServerUrls = new List<PublishedServerUriOverride>();
             _networkEventLock = new object();
             _networkEventLock = new object();
             _remoteAddressFilter = new List<IPNetwork>();
             _remoteAddressFilter = new List<IPNetwork>();
 
 
@@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
         /// <summary>
         /// <summary>
         /// Gets the Published server override list.
         /// Gets the Published server override list.
         /// </summary>
         /// </summary>
-        public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls;
+        public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         public void Dispose()
         public void Dispose()
@@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
             {
             {
                 if (!_eventfire)
                 if (!_eventfire)
                 {
                 {
-                    _logger.LogDebug("Network Address Change Event.");
                     // As network events tend to fire one after the other only fire once every second.
                     // As network events tend to fire one after the other only fire once every second.
                     _eventfire = true;
                     _eventfire = true;
                     OnNetworkChange();
                     OnNetworkChange();
@@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
                 }
                 }
                 else
                 else
                 {
                 {
-                    InitialiseInterfaces();
-                    InitialiseLan(networkConfig);
+                    InitializeInterfaces();
+                    InitializeLan(networkConfig);
                     EnforceBindSettings(networkConfig);
                     EnforceBindSettings(networkConfig);
                 }
                 }
 
 
+                PrintNetworkInformation(networkConfig);
                 NetworkChanged?.Invoke(this, EventArgs.Empty);
                 NetworkChanged?.Invoke(this, EventArgs.Empty);
             }
             }
             finally
             finally
@@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
         /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
         /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
         /// Generate a list of all active mac addresses that aren't loopback addresses.
         /// Generate a list of all active mac addresses that aren't loopback addresses.
         /// </summary>
         /// </summary>
-        private void InitialiseInterfaces()
+        private void InitializeInterfaces()
         {
         {
             lock (_initLock)
             lock (_initLock)
             {
             {
@@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
                 try
                 try
                 {
                 {
                     var nics = NetworkInterface.GetAllNetworkInterfaces()
                     var nics = NetworkInterface.GetAllNetworkInterfaces()
-                        .Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
+                        .Where(i => i.OperationalStatus == OperationalStatus.Up);
 
 
                     foreach (NetworkInterface adapter in nics)
                     foreach (NetworkInterface adapter in nics)
                     {
                     {
@@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
                             {
                             {
                                 if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
                                 if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
                                 {
                                 {
-                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
-                                    interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
-                                    interfaceObject.Name = adapter.Name;
+                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+                                    {
+                                        Index = ipProperties.GetIPv4Properties().Index,
+                                        Name = adapter.Name,
+                                        SupportsMulticast = adapter.SupportsMulticast
+                                    };
 
 
                                     interfaces.Add(interfaceObject);
                                     interfaces.Add(interfaceObject);
                                 }
                                 }
                                 else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
                                 else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
                                 {
                                 {
-                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
-                                    interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
-                                    interfaceObject.Name = adapter.Name;
+                                    var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
+                                    {
+                                        Index = ipProperties.GetIPv6Properties().Index,
+                                        Name = adapter.Name,
+                                        SupportsMulticast = adapter.SupportsMulticast
+                                    };
 
 
                                     interfaces.Add(interfaceObject);
                                     interfaces.Add(interfaceObject);
                                 }
                                 }
                             }
                             }
                         }
                         }
-#pragma warning disable CA1031 // Do not catch general exception types
                         catch (Exception ex)
                         catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
                         {
                         {
                             // Ignore error, and attempt to continue.
                             // Ignore error, and attempt to continue.
                             _logger.LogError(ex, "Error encountered parsing interfaces.");
                             _logger.LogError(ex, "Error encountered parsing interfaces.");
                         }
                         }
                     }
                     }
                 }
                 }
-#pragma warning disable CA1031 // Do not catch general exception types
                 catch (Exception ex)
                 catch (Exception ex)
-#pragma warning restore CA1031 // Do not catch general exception types
                 {
                 {
                     _logger.LogError(ex, "Error obtaining interfaces.");
                     _logger.LogError(ex, "Error obtaining interfaces.");
                 }
                 }
@@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
                 {
                 {
                     _logger.LogWarning("No interface information available. Using loopback interface(s).");
                     _logger.LogWarning("No interface information available. Using loopback interface(s).");
 
 
-                    if (IsIPv4Enabled && !IsIPv6Enabled)
+                    if (IsIPv4Enabled)
                     {
                     {
-                        interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo"));
+                        interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
                     }
                     }
 
 
-                    if (!IsIPv4Enabled && IsIPv6Enabled)
+                    if (IsIPv6Enabled)
                     {
                     {
-                        interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo"));
+                        interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
                     }
                     }
                 }
                 }
 
 
@@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Initialises internal LAN cache.
+        /// Initializes internal LAN cache.
         /// </summary>
         /// </summary>
-        private void InitialiseLan(NetworkConfiguration config)
+        private void InitializeLan(NetworkConfiguration config)
         {
         {
             lock (_initLock)
             lock (_initLock)
             {
             {
@@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
                 _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
                 _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
                     ? excludedSubnets
                     ? excludedSubnets
                     : new List<IPNetwork>();
                     : new List<IPNetwork>();
-
-                _logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
-                _logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
-                _logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
             }
             }
         }
         }
 
 
@@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
                         .ToHashSet();
                         .ToHashSet();
                     interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
                     interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
 
 
-                    if (bindAddresses.Contains(IPAddress.Loopback))
+                    if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
                     {
                     {
                         interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
                         interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
                     }
                     }
 
 
-                    if (bindAddresses.Contains(IPAddress.IPv6Loopback))
+                    if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
                     {
                     {
                         interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
                         interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
                     }
                     }
@@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
                     interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
                     interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
                 }
                 }
 
 
-                _logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
                 _interfaces = interfaces;
                 _interfaces = interfaces;
             }
             }
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Initialises the remote address values.
+        /// Initializes the remote address values.
         /// </summary>
         /// </summary>
-        private void InitialiseRemote(NetworkConfiguration config)
+        private void InitializeRemote(NetworkConfiguration config)
         {
         {
             lock (_initLock)
             lock (_initLock)
             {
             {
@@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
         /// format is subnet=ipaddress|host|uri
         /// format is subnet=ipaddress|host|uri
         /// when subnet = 0.0.0.0, any external address matches.
         /// when subnet = 0.0.0.0, any external address matches.
         /// </summary>
         /// </summary>
-        private void InitialiseOverrides(NetworkConfiguration config)
+        private void InitializeOverrides(NetworkConfiguration config)
         {
         {
             lock (_initLock)
             lock (_initLock)
             {
             {
-                var publishedServerUrls = new Dictionary<IPData, string>();
-                var overrides = config.PublishedServerUriBySubnet;
+                var publishedServerUrls = new List<PublishedServerUriOverride>();
+
+                // Prefer startup configuration.
+                var startupOverrideKey = _startupConfig[AddressOverrideKey];
+                if (!string.IsNullOrEmpty(startupOverrideKey))
+                {
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.Any, Network.IPv4Any),
+                            startupOverrideKey,
+                            true,
+                            true));
+                    publishedServerUrls.Add(
+                        new PublishedServerUriOverride(
+                            new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+                            startupOverrideKey,
+                            true,
+                            true));
+                    _publishedServerUrls = publishedServerUrls;
+                    return;
+                }
 
 
+                var overrides = config.PublishedServerUriBySubnet;
                 foreach (var entry in overrides)
                 foreach (var entry in overrides)
                 {
                 {
                     var parts = entry.Split('=');
                     var parts = entry.Split('=');
@@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
                     var identifier = parts[0];
                     var identifier = parts[0];
                     if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
                     if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement;
+                        // Drop any other overrides in case an "all" override exists
+                        publishedServerUrls.Clear();
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                new IPData(IPAddress.Any, Network.IPv4Any),
+                                replacement,
+                                true,
+                                true));
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+                                replacement,
+                                true,
+                                true));
+                        break;
                     }
                     }
                     else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
                     else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
                     {
                     {
-                        publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
-                        publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                new IPData(IPAddress.Any, Network.IPv4Any),
+                                replacement,
+                                false,
+                                true));
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                new IPData(IPAddress.IPv6Any, Network.IPv6Any),
+                                replacement,
+                                false,
+                                true));
                     }
                     }
                     else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
                     else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
                     {
                     {
                         foreach (var lan in _lanSubnets)
                         foreach (var lan in _lanSubnets)
                         {
                         {
                             var lanPrefix = lan.Prefix;
                             var lanPrefix = lan.Prefix;
-                            publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement;
+                            publishedServerUrls.Add(
+                                new PublishedServerUriOverride(
+                                    new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
+                                    replacement,
+                                    true,
+                                    false));
                         }
                         }
                     }
                     }
                     else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
                     else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
                     {
                     {
                         var data = new IPData(result.Prefix, result);
                         var data = new IPData(result.Prefix, result);
-                        publishedServerUrls[data] = replacement;
+                        publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                data,
+                                replacement,
+                                true,
+                                true));
                     }
                     }
                     else if (TryParseInterface(identifier, out var ifaces))
                     else if (TryParseInterface(identifier, out var ifaces))
                     {
                     {
                         foreach (var iface in ifaces)
                         foreach (var iface in ifaces)
                         {
                         {
-                            publishedServerUrls[iface] = replacement;
+                            publishedServerUrls.Add(
+                            new PublishedServerUriOverride(
+                                iface,
+                                replacement,
+                                true,
+                                true));
                         }
                         }
                     }
                     }
                     else
                     else
@@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
         }
         }
 
 
         /// <summary>
         /// <summary>
-        /// Reloads all settings and re-initialises the instance.
+        /// Reloads all settings and re-Initializes the instance.
         /// </summary>
         /// </summary>
         /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
         /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
         public void UpdateSettings(object configuration)
         public void UpdateSettings(object configuration)
@@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
             var config = (NetworkConfiguration)configuration;
             var config = (NetworkConfiguration)configuration;
             HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
             HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
 
 
-            InitialiseLan(config);
-            InitialiseRemote(config);
+            InitializeLan(config);
+            InitializeRemote(config);
 
 
             if (string.IsNullOrEmpty(MockNetworkSettings))
             if (string.IsNullOrEmpty(MockNetworkSettings))
             {
             {
-                InitialiseInterfaces();
+                InitializeInterfaces();
             }
             }
             else // Used in testing only.
             else // Used in testing only.
             {
             {
@@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
                         var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
                         var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
                         if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
                         if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
                         {
                         {
-                            var data = new IPData(address, subnet, parts[2]);
-                            data.Index = index;
+                            var data = new IPData(address, subnet, parts[2])
+                            {
+                                Index = index
+                            };
                             interfaces.Add(data);
                             interfaces.Add(data);
                         }
                         }
                     }
                     }
@@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
             }
             }
 
 
             EnforceBindSettings(config);
             EnforceBindSettings(config);
-            InitialiseOverrides(config);
+            InitializeOverrides(config);
+
+            PrintNetworkInformation(config, false);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
         /// <inheritdoc/>
         /// <inheritdoc/>
         public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
         public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
         {
         {
-            if (_interfaces.Count != 0)
+            if (_interfaces.Count > 0 || individualInterfaces)
             {
             {
                 return _interfaces;
                 return _interfaces;
             }
             }
 
 
             // No bind address and no exclusions, so listen on all interfaces.
             // No bind address and no exclusions, so listen on all interfaces.
             var result = new List<IPData>();
             var result = new List<IPData>();
-
-            if (individualInterfaces)
-            {
-                result.AddRange(_interfaces);
-                return result;
-            }
-
             if (IsIPv4Enabled && IsIPv6Enabled)
             if (IsIPv4Enabled && IsIPv6Enabled)
             {
             {
                 // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
                 // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
             bindPreference = string.Empty;
             bindPreference = string.Empty;
             int? port = null;
             int? port = null;
 
 
-            var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
-                                                || x.Key.Address.Equals(IPAddress.IPv6Any)
-                                                || x.Key.Subnet.Contains(source))
-                                            .DistinctBy(x => x.Key)
-                                            .OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
-                                                || x.Key.Address.Equals(IPAddress.IPv6Any))
+            // Only consider subnets including the source IP, prefering specific overrides
+            List<PublishedServerUriOverride> validPublishedServerUrls;
+            if (!isInExternalSubnet)
+            {
+                // Only use matching internal subnets
+                // Prefer more specific (bigger subnet prefix) overrides
+                validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
+                                            .OrderByDescending(x => x.Data.Subnet.PrefixLength)
+                                            .ToList();
+            }
+            else
+            {
+                // Only use matching external subnets
+                // Prefer more specific (bigger subnet prefix) overrides
+                validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
+                                            .OrderByDescending(x => x.Data.Subnet.PrefixLength)
                                             .ToList();
                                             .ToList();
+            }
 
 
-            // Check for user override.
             foreach (var data in validPublishedServerUrls)
             foreach (var data in validPublishedServerUrls)
             {
             {
-                if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
-                {
-                    // External.
-                    bindPreference = data.Value;
-                    break;
-                }
-
-                // Get address interface.
-                var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address));
+                // Get interface matching override subnet
+                var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
 
 
                 if (intf?.Address is not null)
                 if (intf?.Address is not null)
                 {
                 {
-                    // Match IP address.
-                    bindPreference = data.Value;
+                    // If matching interface is found, use override
+                    bindPreference = data.OverrideUri;
                     break;
                     break;
                 }
                 }
             }
             }
@@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
                 return false;
                 return false;
             }
             }
 
 
-            // Has it got a port defined?
+            // Handle override specifying port
             var parts = bindPreference.Split(':');
             var parts = bindPreference.Split(':');
             if (parts.Length > 1)
             if (parts.Length > 1)
             {
             {
@@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
                 {
                 {
                     bindPreference = parts[0];
                     bindPreference = parts[0];
                     port = p;
                     port = p;
+                    _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
+                    return true;
                 }
                 }
             }
             }
 
 
-            if (port is not null)
-            {
-                _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
-            }
-            else
-            {
-                _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
-            }
-
+            _logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
             return true;
             return true;
         }
         }
 
 
@@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
             _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
             _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
             return true;
             return true;
         }
         }
+
+        private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
+        {
+            var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
+            if (_logger.IsEnabled(logLevel))
+            {
+                _logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+                _logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
+                _logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
+                _logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
+                _logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
+                _logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
+            }
+        }
     }
     }
 }
 }

+ 0 - 1
Jellyfin.Server.Implementations/Events/Consumers/Security/AuthenticationSucceededLogger.cs

@@ -1,7 +1,6 @@
 using System.Globalization;
 using System.Globalization;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
-using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Controller.Events.Authentication;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Activity;

+ 1 - 2
Jellyfin.Server.Implementations/Events/EventingServiceCollectionExtensions.cs

@@ -1,5 +1,4 @@
-using Jellyfin.Data.Events;
-using Jellyfin.Data.Events.System;
+using Jellyfin.Data.Events.System;
 using Jellyfin.Data.Events.Users;
 using Jellyfin.Data.Events.Users;
 using Jellyfin.Server.Implementations.Events.Consumers.Library;
 using Jellyfin.Server.Implementations.Events.Consumers.Library;
 using Jellyfin.Server.Implementations.Events.Consumers.Security;
 using Jellyfin.Server.Implementations.Events.Consumers.Security;

+ 5 - 0
Jellyfin.Server.Implementations/JellyfinDbContext.cs

@@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
     /// </summary>
     /// </summary>
     public DbSet<User> Users => Set<User>();
     public DbSet<User> Users => Set<User>();
 
 
+    /// <summary>
+    /// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
+    /// </summary>
+    public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
+
     /*public DbSet<Artwork> Artwork => Set<Artwork>();
     /*public DbSet<Artwork> Artwork => Set<Artwork>();
 
 
     public DbSet<Book> Books => Set<Book>();
     public DbSet<Book> Books => Set<Book>();

+ 681 - 0
Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs

@@ -0,0 +1,681 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDbContext))]
+    [Migration("20230626233818_AddTrickplayInfos")]
+    partial class AddTrickplayInfos
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client", "Key")
+                        .IsUnique();
+
+                    b.ToTable("CustomItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("AccessToken")
+                        .IsUnique();
+
+                    b.ToTable("ApiKeys");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId");
+
+                    b.HasIndex("AccessToken", "DateLastActivity");
+
+                    b.HasIndex("DeviceId", "DateLastActivity");
+
+                    b.HasIndex("UserId", "DeviceId");
+
+                    b.ToTable("Devices");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT")
+                        .UseCollation("NOCASE");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Username")
+                        .IsUnique();
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Navigation("AccessSchedules");
+
+                    b.Navigation("DisplayPreferences");
+
+                    b.Navigation("ItemDisplayPreferences");
+
+                    b.Navigation("Permissions");
+
+                    b.Navigation("Preferences");
+
+                    b.Navigation("ProfileImage");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 40 - 0
Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.cs

@@ -0,0 +1,40 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class AddTrickplayInfos : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "TrickplayInfos",
+                columns: table => new
+                {
+                    ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
+                    Width = table.Column<int>(type: "INTEGER", nullable: false),
+                    Height = table.Column<int>(type: "INTEGER", nullable: false),
+                    TileWidth = table.Column<int>(type: "INTEGER", nullable: false),
+                    TileHeight = table.Column<int>(type: "INTEGER", nullable: false),
+                    ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false),
+                    Interval = table.Column<int>(type: "INTEGER", nullable: false),
+                    Bandwidth = table.Column<int>(type: "INTEGER", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
+                });
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "TrickplayInfos");
+        }
+    }
+}

+ 654 - 0
Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs

@@ -0,0 +1,654 @@
+// <auto-generated />
+using System;
+using Jellyfin.Server.Implementations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    [DbContext(typeof(JellyfinDbContext))]
+    [Migration("20230923170422_UserCastReceiver")]
+    partial class UserCastReceiver
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DayOfWeek")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<double>("EndHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<double>("StartHour")
+                        .HasColumnType("REAL");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("AccessSchedules");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("ItemId")
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("LogSeverity")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Overview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("ShortOverview")
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Type")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Key")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client", "Key")
+                        .IsUnique();
+
+                    b.ToTable("CustomItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ChromecastVersion")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DashboardTheme")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("EnableNextVideoInfoOverlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ScrollDirection")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowBackdrop")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("ShowSidebar")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipBackwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SkipForwardLength")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("TvHome")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "ItemId", "Client")
+                        .IsUnique();
+
+                    b.ToTable("DisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("DisplayPreferencesId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Order")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DisplayPreferencesId");
+
+                    b.ToTable("HomeSection");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime>("LastModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasMaxLength(512)
+                        .HasColumnType("TEXT");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("ImageInfos");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Client")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("IndexBy")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("RememberIndexing")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSorting")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SortBy")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SortOrder")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("ViewType")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("ItemDisplayPreferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Permission_Permissions_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("Value")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Permissions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Kind")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("Preference_Preferences_Guid")
+                        .HasColumnType("TEXT");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid?>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Value")
+                        .IsRequired()
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("UserId", "Kind")
+                        .IsUnique()
+                        .HasFilter("[UserId] IS NOT NULL");
+
+                    b.ToTable("Preferences");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("AccessToken")
+                        .IsUnique();
+
+                    b.ToTable("ApiKeys");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("AccessToken")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AppVersion")
+                        .IsRequired()
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateCreated")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateLastActivity")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime>("DateModified")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasMaxLength(256)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceName")
+                        .IsRequired()
+                        .HasMaxLength(64)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("IsActive")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId");
+
+                    b.HasIndex("AccessToken", "DateLastActivity");
+
+                    b.HasIndex("DeviceId", "DateLastActivity");
+
+                    b.HasIndex("UserId", "DeviceId");
+
+                    b.ToTable("Devices");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("CustomName")
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("DeviceId")
+                        .IsRequired()
+                        .HasColumnType("TEXT");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("DeviceId")
+                        .IsUnique();
+
+                    b.ToTable("DeviceOptions");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Property<Guid>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AudioLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("AuthenticationProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("DisplayCollectionsView")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("DisplayMissingEpisodes")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableAutoLogin")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableLocalPassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableNextEpisodeAutoPlay")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("EnableUserPreferenceAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("HidePlayedInLatest")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<long>("InternalId")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("InvalidLoginAttemptCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<DateTime?>("LastActivityDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<DateTime?>("LastLoginDate")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int?>("LoginAttemptsBeforeLockout")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("MaxActiveSessions")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("MaxParentalAgeRating")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("MustUpdatePassword")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Password")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    b.Property<string>("PasswordResetProviderId")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<bool>("PlayDefaultAudioTrack")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberAudioSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<bool>("RememberSubtitleSelections")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int?>("RemoteClientBitrateLimit")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<uint>("RowVersion")
+                        .IsConcurrencyToken()
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("SubtitleLanguagePreference")
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("SubtitleMode")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("SyncPlayAccess")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<string>("Username")
+                        .IsRequired()
+                        .HasMaxLength(255)
+                        .HasColumnType("TEXT")
+                        .UseCollation("NOCASE");
+
+                    b.HasKey("Id");
+
+                    b.HasIndex("Username")
+                        .IsUnique();
+
+                    b.ToTable("Users");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("AccessSchedules")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("DisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
+                        .WithMany("HomeSections")
+                        .HasForeignKey("DisplayPreferencesId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithOne("ProfileImage")
+                        .HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("ItemDisplayPreferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Permissions")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", null)
+                        .WithMany("Preferences")
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade);
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
+                {
+                    b.HasOne("Jellyfin.Data.Entities.User", "User")
+                        .WithMany()
+                        .HasForeignKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired();
+
+                    b.Navigation("User");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
+                {
+                    b.Navigation("HomeSections");
+                });
+
+            modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
+                {
+                    b.Navigation("AccessSchedules");
+
+                    b.Navigation("DisplayPreferences");
+
+                    b.Navigation("ItemDisplayPreferences");
+
+                    b.Navigation("Permissions");
+
+                    b.Navigation("Preferences");
+
+                    b.Navigation("ProfileImage");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}

+ 29 - 0
Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.cs

@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    /// <inheritdoc />
+    public partial class UserCastReceiver : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "CastReceiverId",
+                table: "Users",
+                type: "TEXT",
+                maxLength: 32,
+                nullable: true);
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "CastReceiverId",
+                table: "Users");
+        }
+    }
+}

+ 37 - 2
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -1,4 +1,4 @@
-// <auto-generated />
+// <auto-generated />
 using System;
 using System;
 using Jellyfin.Server.Implementations;
 using Jellyfin.Server.Implementations;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
@@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
         protected override void BuildModel(ModelBuilder modelBuilder)
         protected override void BuildModel(ModelBuilder modelBuilder)
         {
         {
 #pragma warning disable 612, 618
 #pragma warning disable 612, 618
-            modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
+            modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
 
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
                 {
@@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.ToTable("DeviceOptions");
                     b.ToTable("DeviceOptions");
                 });
                 });
 
 
+            modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
+                {
+                    b.Property<Guid>("ItemId")
+                        .HasColumnType("TEXT");
+
+                    b.Property<int>("Width")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Bandwidth")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Height")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("Interval")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("ThumbnailCount")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileHeight")
+                        .HasColumnType("INTEGER");
+
+                    b.Property<int>("TileWidth")
+                        .HasColumnType("INTEGER");
+
+                    b.HasKey("ItemId", "Width");
+
+                    b.ToTable("TrickplayInfos");
+                });
+
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
                 {
                 {
                     b.Property<Guid>("Id")
                     b.Property<Guid>("Id")
@@ -457,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .HasMaxLength(255)
                         .HasMaxLength(255)
                         .HasColumnType("TEXT");
                         .HasColumnType("TEXT");
 
 
+                    b.Property<string>("CastReceiverId")
+                        .HasMaxLength(32)
+                        .HasColumnType("TEXT");
+
                     b.Property<bool>("DisplayCollectionsView")
                     b.Property<bool>("DisplayCollectionsView")
                         .HasColumnType("INTEGER");
                         .HasColumnType("INTEGER");
 
 

+ 18 - 0
Jellyfin.Server.Implementations/ModelConfiguration/TrickplayInfoConfiguration.cs

@@ -0,0 +1,18 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration
+{
+    /// <summary>
+    /// FluentAPI configuration for the TrickplayInfo entity.
+    /// </summary>
+    public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo>
+    {
+        /// <inheritdoc/>
+        public void Configure(EntityTypeBuilder<TrickplayInfo> builder)
+        {
+            builder.HasKey(info => new { info.ItemId, info.Width });
+        }
+    }
+}

+ 5 - 25
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
         /// <summary>
         /// <summary>
         /// Gets the authorization.
         /// Gets the authorization.
         /// </summary>
         /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
+        /// <param name="httpContext">The HTTP context.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
+        private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
         {
         {
-            var auth = GetAuthorizationDictionary(httpReq);
-            var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
+            var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
 
 
-            httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
+            httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
             return authInfo;
             return authInfo;
         }
         }
 
 
@@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
                 auth.TryGetValue("Token", out token);
                 auth.TryGetValue("Token", out token);
             }
             }
 
 
-#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
             if (string.IsNullOrEmpty(token))
             if (string.IsNullOrEmpty(token))
             {
             {
                 token = headers["X-Emby-Token"];
                 token = headers["X-Emby-Token"];
@@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
                 // Request doesn't contain a token.
                 // Request doesn't contain a token.
                 return authInfo;
                 return authInfo;
             }
             }
-#pragma warning restore CA1508
 
 
             authInfo.HasToken = true;
             authInfo.HasToken = true;
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
             var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
@@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
         /// <summary>
         /// <summary>
         /// Gets the auth.
         /// Gets the auth.
         /// </summary>
         /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
-        /// <returns>Dictionary{System.StringSystem.String}.</returns>
-        private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
-        {
-            var auth = httpReq.Request.Headers["X-Emby-Authorization"];
-
-            if (string.IsNullOrEmpty(auth))
-            {
-                auth = httpReq.Request.Headers[HeaderNames.Authorization];
-            }
-
-            return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
-        }
-
-        /// <summary>
-        /// Gets the auth.
-        /// </summary>
-        /// <param name="httpReq">The HTTP req.</param>
+        /// <param name="httpReq">The HTTP request.</param>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         /// <returns>Dictionary{System.StringSystem.String}.</returns>
         private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
         private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
         {
         {

+ 474 - 0
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs

@@ -0,0 +1,474 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Data.Entities;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Trickplay;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Trickplay;
+
+/// <summary>
+/// ITrickplayManager implementation.
+/// </summary>
+public class TrickplayManager : ITrickplayManager
+{
+    private readonly ILogger<TrickplayManager> _logger;
+    private readonly IMediaEncoder _mediaEncoder;
+    private readonly IFileSystem _fileSystem;
+    private readonly EncodingHelper _encodingHelper;
+    private readonly ILibraryManager _libraryManager;
+    private readonly IServerConfigurationManager _config;
+    private readonly IImageEncoder _imageEncoder;
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IApplicationPaths _appPaths;
+
+    private static readonly SemaphoreSlim _resourcePool = new(1, 1);
+    private static readonly string[] _trickplayImgExtensions = { ".jpg" };
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="TrickplayManager"/> class.
+    /// </summary>
+    /// <param name="logger">The logger.</param>
+    /// <param name="mediaEncoder">The media encoder.</param>
+    /// <param name="fileSystem">The file systen.</param>
+    /// <param name="encodingHelper">The encoding helper.</param>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="config">The server configuration manager.</param>
+    /// <param name="imageEncoder">The image encoder.</param>
+    /// <param name="dbProvider">The database provider.</param>
+    /// <param name="appPaths">The application paths.</param>
+    public TrickplayManager(
+        ILogger<TrickplayManager> logger,
+        IMediaEncoder mediaEncoder,
+        IFileSystem fileSystem,
+        EncodingHelper encodingHelper,
+        ILibraryManager libraryManager,
+        IServerConfigurationManager config,
+        IImageEncoder imageEncoder,
+        IDbContextFactory<JellyfinDbContext> dbProvider,
+        IApplicationPaths appPaths)
+    {
+        _logger = logger;
+        _mediaEncoder = mediaEncoder;
+        _fileSystem = fileSystem;
+        _encodingHelper = encodingHelper;
+        _libraryManager = libraryManager;
+        _config = config;
+        _imageEncoder = imageEncoder;
+        _dbProvider = dbProvider;
+        _appPaths = appPaths;
+    }
+
+    /// <inheritdoc />
+    public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
+    {
+        _logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
+
+        var options = _config.Configuration.TrickplayOptions;
+        foreach (var width in options.WidthResolutions)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
+            await RefreshTrickplayDataInternal(
+                video,
+                replace,
+                width,
+                options,
+                cancellationToken).ConfigureAwait(false);
+        }
+    }
+
+    private async Task RefreshTrickplayDataInternal(
+        Video video,
+        bool replace,
+        int width,
+        TrickplayOptions options,
+        CancellationToken cancellationToken)
+    {
+        if (!CanGenerateTrickplay(video, options.Interval))
+        {
+            return;
+        }
+
+        var imgTempDir = string.Empty;
+        var outputDir = GetTrickplayDirectory(video, width);
+
+        await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
+
+        try
+        {
+            if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
+            {
+                _logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
+                return;
+            }
+
+            // Extract images
+            // Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
+            var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
+
+            if (mediaSource is null)
+            {
+                _logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
+                return;
+            }
+
+            var mediaPath = mediaSource.Path;
+            var mediaStream = mediaSource.VideoStream;
+            var container = mediaSource.Container;
+
+            _logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
+            imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
+                mediaPath,
+                container,
+                mediaSource,
+                mediaStream,
+                width,
+                TimeSpan.FromMilliseconds(options.Interval),
+                options.EnableHwAcceleration,
+                options.ProcessThreads,
+                options.Qscale,
+                options.ProcessPriority,
+                _encodingHelper,
+                cancellationToken).ConfigureAwait(false);
+
+            if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
+            {
+                throw new InvalidOperationException("Null or invalid directory from media encoder.");
+            }
+
+            var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
+                .Select(i => i.FullName)
+                .OrderBy(i => i)
+                .ToList();
+
+            // Create tiles
+            var trickplayInfo = CreateTiles(images, width, options, outputDir);
+
+            // Save tiles info
+            try
+            {
+                if (trickplayInfo is not null)
+                {
+                    trickplayInfo.ItemId = video.Id;
+                    await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
+
+                    _logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
+                }
+                else
+                {
+                    throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error while saving trickplay tiles info.");
+
+                // Make sure no files stay in metadata folders on failure
+                // if tiles info wasn't saved.
+                Directory.Delete(outputDir, true);
+            }
+        }
+        catch (Exception ex)
+        {
+            _logger.LogError(ex, "Error creating trickplay images.");
+        }
+        finally
+        {
+            _resourcePool.Release();
+
+            if (!string.IsNullOrEmpty(imgTempDir))
+            {
+                Directory.Delete(imgTempDir, true);
+            }
+        }
+    }
+
+    /// <inheritdoc />
+    public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
+    {
+        if (images.Count == 0)
+        {
+            throw new ArgumentException("Can't create trickplay from 0 images.");
+        }
+
+        var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
+        Directory.CreateDirectory(workDir);
+
+        var trickplayInfo = new TrickplayInfo
+        {
+            Width = width,
+            Interval = options.Interval,
+            TileWidth = options.TileWidth,
+            TileHeight = options.TileHeight,
+            ThumbnailCount = images.Count,
+            // Set during image generation
+            Height = 0,
+            Bandwidth = 0
+        };
+
+        /*
+         * Generate trickplay tiles from sets of thumbnails
+         */
+        var imageOptions = new ImageCollageOptions
+        {
+            Width = trickplayInfo.TileWidth,
+            Height = trickplayInfo.TileHeight
+        };
+
+        var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+        var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
+
+        for (int i = 0; i < requiredTiles; i++)
+        {
+            // Set output/input paths
+            var tilePath = Path.Combine(workDir, $"{i}.jpg");
+
+            imageOptions.OutputPath = tilePath;
+            imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
+
+            // Generate image and use returned height for tiles info
+            var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
+            if (trickplayInfo.Height == 0)
+            {
+                trickplayInfo.Height = height;
+            }
+
+            // Update bitrate
+            var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
+            trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
+        }
+
+        /*
+         * Move trickplay tiles to output directory
+         */
+        Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
+
+        // Replace existing tiles if they already exist
+        if (Directory.Exists(outputDir))
+        {
+            Directory.Delete(outputDir, true);
+        }
+
+        MoveDirectory(workDir, outputDir);
+
+        return trickplayInfo;
+    }
+
+    private bool CanGenerateTrickplay(Video video, int interval)
+    {
+        var videoType = video.VideoType;
+        if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
+        {
+            return false;
+        }
+
+        if (video.IsPlaceHolder)
+        {
+            return false;
+        }
+
+        if (video.IsShortcut)
+        {
+            return false;
+        }
+
+        if (!video.IsCompleteMedia)
+        {
+            return false;
+        }
+
+        if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
+        {
+            return false;
+        }
+
+        var libraryOptions = _libraryManager.GetLibraryOptions(video);
+        if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
+        {
+            return false;
+        }
+
+        // Can't extract images if there are no video streams
+        return video.GetMediaStreams().Count > 0;
+    }
+
+    /// <inheritdoc />
+    public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId)
+    {
+        var trickplayResolutions = new Dictionary<int, TrickplayInfo>();
+
+        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+        await using (dbContext.ConfigureAwait(false))
+        {
+            var trickplayInfos = await dbContext.TrickplayInfos
+                .AsNoTracking()
+                .Where(i => i.ItemId.Equals(itemId))
+                .ToListAsync()
+                .ConfigureAwait(false);
+
+            foreach (var info in trickplayInfos)
+            {
+                trickplayResolutions[info.Width] = info;
+            }
+        }
+
+        return trickplayResolutions;
+    }
+
+    /// <inheritdoc />
+    public async Task SaveTrickplayInfo(TrickplayInfo info)
+    {
+        var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+        await using (dbContext.ConfigureAwait(false))
+        {
+            var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
+            if (oldInfo is not null)
+            {
+                dbContext.TrickplayInfos.Remove(oldInfo);
+            }
+
+            dbContext.Add(info);
+
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+        }
+    }
+
+    /// <inheritdoc />
+    public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item)
+    {
+        var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>();
+        foreach (var mediaSource in item.GetMediaSources(false))
+        {
+            var mediaSourceId = Guid.Parse(mediaSource.Id);
+            var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
+
+            if (trickplayResolutions.Count > 0)
+            {
+                trickplayManifest[mediaSource.Id] = trickplayResolutions;
+            }
+        }
+
+        return trickplayManifest;
+    }
+
+    /// <inheritdoc />
+    public string GetTrickplayTilePath(BaseItem item, int width, int index)
+    {
+        return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
+    }
+
+    /// <inheritdoc />
+    public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey)
+    {
+        var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
+        if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
+        {
+            var builder = new StringBuilder(128);
+
+            if (trickplayInfo.ThumbnailCount > 0)
+            {
+                const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
+                const string decimalFormat = "{0:0.###}";
+
+                var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
+                var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
+                var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
+                var thumbnailDuration = trickplayInfo.Interval / 1000d;
+                var infDuration = thumbnailDuration * thumbnailsPerTile;
+                var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
+
+                builder
+                    .AppendLine("#EXTM3U")
+                    .Append("#EXT-X-TARGETDURATION:")
+                    .AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
+                    .AppendLine("#EXT-X-VERSION:7")
+                    .AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
+                    .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
+                    .AppendLine("#EXT-X-IMAGES-ONLY");
+
+                for (int i = 0; i < tileCount; i++)
+                {
+                    // All tiles prior to the last must contain full amount of thumbnails (no black).
+                    if (i == tileCount - 1)
+                    {
+                        thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
+                        infDuration = thumbnailDuration * thumbnailsPerTile;
+                    }
+
+                    // EXTINF
+                    builder
+                        .Append("#EXTINF:")
+                        .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
+                        .AppendLine(",");
+
+                    // EXT-X-TILES
+                    builder
+                        .Append("#EXT-X-TILES:RESOLUTION=")
+                        .Append(resolution)
+                        .Append(",LAYOUT=")
+                        .Append(layout)
+                        .Append(",DURATION=")
+                        .AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
+                        .AppendLine();
+
+                    // URL
+                    builder
+                        .AppendFormat(
+                            CultureInfo.InvariantCulture,
+                            urlFormat,
+                            width.ToString(CultureInfo.InvariantCulture),
+                            i.ToString(CultureInfo.InvariantCulture),
+                            itemId.ToString("N"),
+                            apiKey)
+                        .AppendLine();
+                }
+
+                builder.AppendLine("#EXT-X-ENDLIST");
+                return builder.ToString();
+            }
+        }
+
+        return null;
+    }
+
+    private string GetTrickplayDirectory(BaseItem item, int? width = null)
+    {
+        var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+        return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
+    }
+
+    private void MoveDirectory(string source, string destination)
+    {
+        try
+        {
+            Directory.Move(source, destination);
+        }
+        catch (IOException)
+        {
+            // Cross device move requires a copy
+            Directory.CreateDirectory(destination);
+            foreach (string file in Directory.GetFiles(source))
+            {
+                File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
+            }
+
+            Directory.Delete(source, true);
+        }
+    }
+}

+ 20 - 3
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -15,6 +15,7 @@ using MediaBrowser.Common;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Authentication;
 using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Events;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -43,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
         private readonly InvalidAuthProvider _invalidAuthProvider;
         private readonly InvalidAuthProvider _invalidAuthProvider;
         private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
         private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
         private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
         private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
+        private readonly IServerConfigurationManager _serverConfigurationManager;
 
 
         private readonly IDictionary<Guid, User> _users;
         private readonly IDictionary<Guid, User> _users;
 
 
@@ -55,13 +57,15 @@ namespace Jellyfin.Server.Implementations.Users
         /// <param name="appHost">The application host.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
+        /// <param name="serverConfigurationManager">The system config manager.</param>
         public UserManager(
         public UserManager(
             IDbContextFactory<JellyfinDbContext> dbProvider,
             IDbContextFactory<JellyfinDbContext> dbProvider,
             IEventManager eventManager,
             IEventManager eventManager,
             INetworkManager networkManager,
             INetworkManager networkManager,
             IApplicationHost appHost,
             IApplicationHost appHost,
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
-            ILogger<UserManager> logger)
+            ILogger<UserManager> logger,
+            IServerConfigurationManager serverConfigurationManager)
         {
         {
             _dbProvider = dbProvider;
             _dbProvider = dbProvider;
             _eventManager = eventManager;
             _eventManager = eventManager;
@@ -69,6 +73,7 @@ namespace Jellyfin.Server.Implementations.Users
             _appHost = appHost;
             _appHost = appHost;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _logger = logger;
             _logger = logger;
+            _serverConfigurationManager = serverConfigurationManager;
 
 
             _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
             _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
             _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
             _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
@@ -103,7 +108,7 @@ namespace Jellyfin.Server.Implementations.Users
         // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
         // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
         // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
         // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
         // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
         // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
-        [GeneratedRegex("^[\\w\\ \\-'._@]+$")]
+        [GeneratedRegex(@"^[\w\ \-'._@]+$")]
         private static partial Regex ValidUsernameRegex();
         private static partial Regex ValidUsernameRegex();
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
@@ -288,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
         public UserDto GetUserDto(User user, string? remoteEndPoint = null)
         public UserDto GetUserDto(User user, string? remoteEndPoint = null)
         {
         {
             var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
             var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
+            var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
             return new UserDto
             return new UserDto
             {
             {
                 Name = user.Username,
                 Name = user.Username,
@@ -315,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
                     OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
                     OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
                     GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
                     GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
                     MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
                     MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
-                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes)
+                    LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
+                    CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
+                        ? castReceiverApplications.FirstOrDefault()?.Id
+                        : castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id
+                          ?? castReceiverApplications.FirstOrDefault()?.Id
                 },
                 },
                 Policy = new UserPolicy
                 Policy = new UserPolicy
                 {
                 {
@@ -604,6 +614,13 @@ namespace Jellyfin.Server.Implementations.Users
                 user.RememberSubtitleSelections = config.RememberSubtitleSelections;
                 user.RememberSubtitleSelections = config.RememberSubtitleSelections;
                 user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
                 user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
 
 
+                // Only set cast receiver id if it is passed in and it exists in the server config.
+                if (!string.IsNullOrEmpty(config.CastReceiverId)
+                    && _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
+                {
+                    user.CastReceiverId = config.CastReceiverId;
+                }
+
                 user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
                 user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
                 user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
                 user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
                 user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
                 user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);

+ 3 - 0
Jellyfin.Server/CoreAppHost.cs

@@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
 using Jellyfin.Server.Implementations.Devices;
 using Jellyfin.Server.Implementations.Devices;
 using Jellyfin.Server.Implementations.Events;
 using Jellyfin.Server.Implementations.Events;
 using Jellyfin.Server.Implementations.Security;
 using Jellyfin.Server.Implementations.Security;
+using Jellyfin.Server.Implementations.Trickplay;
 using Jellyfin.Server.Implementations.Users;
 using Jellyfin.Server.Implementations.Users;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.BaseItemManager;
 using MediaBrowser.Controller.BaseItemManager;
@@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Security;
 using MediaBrowser.Controller.Security;
+using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Model.Activity;
 using MediaBrowser.Providers.Lyric;
 using MediaBrowser.Providers.Lyric;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
@@ -78,6 +80,7 @@ namespace Jellyfin.Server
             serviceCollection.AddSingleton<IUserManager, UserManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
             serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
             serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
             serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
             serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
+            serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
 
 
             // TODO search the assemblies instead of adding them manually?
             // TODO search the assemblies instead of adding them manually?
             serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
             serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();

+ 1 - 1
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
                         AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
                         AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
                     }
                     }
                 }
                 }
-                else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses))
+                else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
                 {
                 {
                     foreach (var address in addresses)
                     foreach (var address in addresses)
                     {
                     {

+ 1 - 2
Jellyfin.Server/Extensions/WebHostBuilderExtensions.cs

@@ -3,7 +3,6 @@ using System.IO;
 using System.Net;
 using System.Net;
 using Jellyfin.Server.Helpers;
 using Jellyfin.Server.Helpers;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
-using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Extensions;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
@@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
         return builder
         return builder
             .UseKestrel((builderContext, options) =>
             .UseKestrel((builderContext, options) =>
             {
             {
-                var addresses = appHost.NetManager.GetAllBindInterfaces();
+                var addresses = appHost.NetManager.GetAllBindInterfaces(true);
 
 
                 bool flagged = false;
                 bool flagged = false;
                 foreach (var netAdd in addresses)
                 foreach (var netAdd in addresses)

+ 2 - 1
Jellyfin.Server/Migrations/MigrationRunner.cs

@@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
             typeof(Routines.RemoveDownloadImagesInAdvance),
             typeof(Routines.RemoveDownloadImagesInAdvance),
             typeof(Routines.MigrateAuthenticationDb),
             typeof(Routines.MigrateAuthenticationDb),
             typeof(Routines.FixPlaylistOwner),
             typeof(Routines.FixPlaylistOwner),
-            typeof(Routines.MigrateRatingLevels)
+            typeof(Routines.MigrateRatingLevels),
+            typeof(Routines.AddDefaultCastReceivers)
         };
         };
 
 
         /// <summary>
         /// <summary>

+ 55 - 0
Jellyfin.Server/Migrations/Routines/AddDefaultCastReceivers.cs

@@ -0,0 +1,55 @@
+using System;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Model.System;
+
+namespace Jellyfin.Server.Migrations.Routines;
+
+/// <summary>
+/// Migration to add the default cast receivers to the system config.
+/// </summary>
+public class AddDefaultCastReceivers : IMigrationRoutine
+{
+    private readonly IServerConfigurationManager _serverConfigurationManager;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="AddDefaultCastReceivers"/> class.
+    /// </summary>
+    /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
+    public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager)
+    {
+        _serverConfigurationManager = serverConfigurationManager;
+    }
+
+    /// <inheritdoc />
+    public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
+
+    /// <inheritdoc />
+    public string Name => "AddDefaultCastReceivers";
+
+    /// <inheritdoc />
+    public bool PerformOnNewInstall => true;
+
+    /// <inheritdoc />
+    public void Perform()
+    {
+        // Only add if receiver list is empty.
+        if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0)
+        {
+            _serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[]
+            {
+                new()
+                {
+                    Id = "F007D354",
+                    Name = "Stable"
+                },
+                new()
+                {
+                    Id = "6F511C87",
+                    Name = "Unstable"
+                }
+            };
+
+            _serverConfigurationManager.SaveConfiguration();
+        }
+    }
+}

+ 0 - 1
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs

@@ -3,7 +3,6 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using Emby.Server.Implementations.Data;
 using Emby.Server.Implementations.Data;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using Microsoft.Data.Sqlite;
 using Microsoft.Data.Sqlite;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;

+ 2 - 18
Jellyfin.Server/Startup.cs

@@ -1,10 +1,10 @@
 using System;
 using System;
-using System.Globalization;
 using System.Net;
 using System.Net;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Net.Http.Headers;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Text;
 using System.Text;
+using Emby.Dlna.Extensions;
 using Jellyfin.Api.Middleware;
 using Jellyfin.Api.Middleware;
 using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.MediaEncoding.Hls.Extensions;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Networking.Configuration;
@@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.FileProviders;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Hosting;
-using Microsoft.VisualBasic;
 using Prometheus;
 using Prometheus;
 
 
 namespace Jellyfin.Server
 namespace Jellyfin.Server
@@ -120,26 +119,11 @@ namespace Jellyfin.Server
                 })
                 })
                 .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
                 .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
 
 
-            services.AddHttpClient(NamedClient.Dlna, c =>
-                {
-                    c.DefaultRequestHeaders.UserAgent.ParseAdd(
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            "{0}/{1} UPnP/1.0 {2}/{3}",
-                            Environment.OSVersion.Platform,
-                            Environment.OSVersion,
-                            _serverApplicationHost.Name,
-                            _serverApplicationHost.ApplicationVersionString));
-
-                    c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
-                    c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from?
-                })
-                .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
-
             services.AddHealthChecks()
             services.AddHealthChecks()
                 .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
                 .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
 
 
             services.AddHlsPlaylistGenerator();
             services.AddHlsPlaylistGenerator();
+            services.AddDlnaServices(_serverApplicationHost);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 4 - 56
MediaBrowser.Common/Extensions/ProcessExtensions.cs

@@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
         /// </summary>
         /// </summary>
         /// <param name="process">The process to wait for.</param>
         /// <param name="process">The process to wait for.</param>
         /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
         /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
-        /// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
-        /// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
-        public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
+        /// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
+        /// <exception cref="OperationCanceledException">The timeout ended.</exception>
+        public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
         {
         {
             using (var cancelTokenSource = new CancellationTokenSource(timeout))
             using (var cancelTokenSource = new CancellationTokenSource(timeout))
             {
             {
-                return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Asynchronously wait for the process to exit.
-        /// </summary>
-        /// <param name="process">The process to wait for.</param>
-        /// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
-        /// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
-        public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
-        {
-            if (!process.EnableRaisingEvents)
-            {
-                throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
-            }
-
-            // Add an event handler for the process exit event
-            var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
-            process.Exited += (_, _) => tcs.TrySetResult(true);
-
-            // Return immediately if the process has already exited
-            if (process.HasExitedSafe())
-            {
-                return true;
-            }
-
-            // Register with the cancellation token then await
-            using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
-            {
-                return await tcs.Task.ConfigureAwait(false);
-            }
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the associated process has been terminated using
-        /// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
-        /// associated with the <see cref="Process"/>.
-        /// </summary>
-        /// <param name="process">The process to check the exit status for.</param>
-        /// <returns>
-        /// True if the operating system process referenced by the <see cref="Process"/> component has
-        /// terminated, or if there is no associated operating system process; otherwise, false.
-        /// </returns>
-        private static bool HasExitedSafe(this Process process)
-        {
-            try
-            {
-                return process.HasExited;
-            }
-            catch (InvalidOperationException)
-            {
-                return true;
+                await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
             }
             }
         }
         }
     }
     }

+ 4 - 20
MediaBrowser.Common/IApplicationHost.cs

@@ -35,21 +35,15 @@ namespace MediaBrowser.Common
         string SystemId { get; }
         string SystemId { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets a value indicating whether this instance has pending kernel reload.
+        /// Gets a value indicating whether this instance has pending changes requiring a restart.
         /// </summary>
         /// </summary>
-        /// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
+        /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
         bool HasPendingRestart { get; }
         bool HasPendingRestart { get; }
 
 
         /// <summary>
         /// <summary>
-        /// Gets a value indicating whether this instance is currently shutting down.
+        /// Gets or sets a value indicating whether the application should restart.
         /// </summary>
         /// </summary>
-        /// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
-        bool IsShuttingDown { get; }
-
-        /// <summary>
-        /// Gets a value indicating whether the application should restart.
-        /// </summary>
-        bool ShouldRestart { get; }
+        bool ShouldRestart { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets the application version.
         /// Gets the application version.
@@ -91,11 +85,6 @@ namespace MediaBrowser.Common
         /// </summary>
         /// </summary>
         void NotifyPendingRestart();
         void NotifyPendingRestart();
 
 
-        /// <summary>
-        /// Restarts this instance.
-        /// </summary>
-        void Restart();
-
         /// <summary>
         /// <summary>
         /// Gets the exports.
         /// Gets the exports.
         /// </summary>
         /// </summary>
@@ -127,11 +116,6 @@ namespace MediaBrowser.Common
         /// <returns>``0.</returns>
         /// <returns>``0.</returns>
         T Resolve<T>();
         T Resolve<T>();
 
 
-        /// <summary>
-        /// Shuts down.
-        /// </summary>
-        void Shutdown();
-
         /// <summary>
         /// <summary>
         /// Initializes this instance.
         /// Initializes this instance.
         /// </summary>
         /// </summary>

+ 0 - 4
MediaBrowser.Common/MediaBrowser.Common.csproj

@@ -38,10 +38,6 @@
     <SymbolPackageFormat>snupkg</SymbolPackageFormat>
     <SymbolPackageFormat>snupkg</SymbolPackageFormat>
   </PropertyGroup>
   </PropertyGroup>
 
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
-  </PropertyGroup>
-
   <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
   <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
     <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
     <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
     <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
     <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

+ 0 - 1
MediaBrowser.Controller/BaseItemManager/BaseItemManager.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Linq;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików