Explorar o código

Merge branch 'master' into fix-fmp4-flac-opus

Cody Robibero %!s(int64=2) %!d(string=hai) anos
pai
achega
4e34c428d8
Modificáronse 100 ficheiros con 1549 adicións e 758 borrados
  1. 0 15
      .github/dependabot.yml
  2. 6 0
      .github/renovate.json
  3. 6 6
      .github/workflows/automation.yml
  4. 5 5
      .github/workflows/codeql-analysis.yml
  5. 8 8
      .github/workflows/commands.yml
  6. 11 11
      .github/workflows/openapi.yml
  7. 1 1
      .github/workflows/repo-stale.yaml
  8. 0 3
      .gitignore
  9. 1 0
      Directory.Build.props
  10. 1 1
      Dockerfile
  11. 1 1
      Dockerfile.arm
  12. 1 1
      Dockerfile.arm64
  13. 1 2
      Emby.Dlna/Eventing/DlnaEventManager.cs
  14. 0 1
      Emby.Dlna/PlayTo/PlayToController.cs
  15. 1 2
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  16. 11 4
      Emby.Naming/Common/NamingOptions.cs
  17. 1 2
      Emby.Notifications/NotificationManager.cs
  18. 14 9
      Emby.Server.Implementations/ApplicationHost.cs
  19. 4 4
      Emby.Server.Implementations/Collections/CollectionManager.cs
  20. 7 0
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  21. 4 4
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  22. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  23. 4 16
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  24. 2 2
      Emby.Server.Implementations/Library/LibraryManager.cs
  25. 2 2
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  26. 11 9
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  27. 10 37
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  28. 20 3
      Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs
  29. 9 6
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  30. 2 5
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  31. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  32. 1 1
      Emby.Server.Implementations/Localization/Core/ar.json
  33. 2 1
      Emby.Server.Implementations/Localization/Core/ca.json
  34. 2 1
      Emby.Server.Implementations/Localization/Core/cs.json
  35. 2 1
      Emby.Server.Implementations/Localization/Core/da.json
  36. 2 1
      Emby.Server.Implementations/Localization/Core/de.json
  37. 2 1
      Emby.Server.Implementations/Localization/Core/el.json
  38. 2 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  39. 2 1
      Emby.Server.Implementations/Localization/Core/es-AR.json
  40. 2 1
      Emby.Server.Implementations/Localization/Core/es-MX.json
  41. 2 1
      Emby.Server.Implementations/Localization/Core/es.json
  42. 4 1
      Emby.Server.Implementations/Localization/Core/et.json
  43. 8 1
      Emby.Server.Implementations/Localization/Core/eu.json
  44. 2 1
      Emby.Server.Implementations/Localization/Core/fi.json
  45. 3 2
      Emby.Server.Implementations/Localization/Core/fr-CA.json
  46. 2 1
      Emby.Server.Implementations/Localization/Core/fr.json
  47. 6 2
      Emby.Server.Implementations/Localization/Core/gl.json
  48. 2 1
      Emby.Server.Implementations/Localization/Core/he.json
  49. 2 1
      Emby.Server.Implementations/Localization/Core/hr.json
  50. 2 1
      Emby.Server.Implementations/Localization/Core/hu.json
  51. 2 1
      Emby.Server.Implementations/Localization/Core/id.json
  52. 2 1
      Emby.Server.Implementations/Localization/Core/it.json
  53. 7 0
      Emby.Server.Implementations/Localization/Core/jbo.json
  54. 3 0
      Emby.Server.Implementations/Localization/Core/km.json
  55. 2 1
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  56. 2 1
      Emby.Server.Implementations/Localization/Core/nb.json
  57. 4 3
      Emby.Server.Implementations/Localization/Core/nl.json
  58. 2 1
      Emby.Server.Implementations/Localization/Core/pt-BR.json
  59. 2 1
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  60. 2 1
      Emby.Server.Implementations/Localization/Core/pt.json
  61. 24 23
      Emby.Server.Implementations/Localization/Core/ro.json
  62. 4 3
      Emby.Server.Implementations/Localization/Core/ru.json
  63. 2 1
      Emby.Server.Implementations/Localization/Core/sk.json
  64. 5 1
      Emby.Server.Implementations/Localization/Core/sq.json
  65. 2 1
      Emby.Server.Implementations/Localization/Core/uk.json
  66. 5 5
      Emby.Server.Implementations/Localization/Core/ur_PK.json
  67. 2 1
      Emby.Server.Implementations/Localization/Core/vi.json
  68. 2 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  69. 2 1
      Emby.Server.Implementations/Localization/Core/zh-HK.json
  70. 3 2
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  71. 3 2
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  72. 2 0
      Emby.Server.Implementations/Plugins/PluginManager.cs
  73. 7 7
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  74. 15 14
      Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs
  75. 11 15
      Emby.Server.Implementations/TV/TVSeriesManager.cs
  76. 1 1
      Jellyfin.Api/Controllers/DisplayPreferencesController.cs
  77. 13 39
      Jellyfin.Api/Controllers/ItemsController.cs
  78. 1 1
      Jellyfin.Api/Controllers/LibraryController.cs
  79. 10 10
      Jellyfin.Api/Controllers/MoviesController.cs
  80. 4 4
      Jellyfin.Api/Controllers/TrailersController.cs
  81. 1 1
      Jellyfin.Api/Helpers/StreamingHelpers.cs
  82. 1 1
      Jellyfin.Api/Jellyfin.Api.csproj
  83. 39 34
      Jellyfin.Server.Implementations/Activity/ActivityManager.cs
  84. 94 74
      Jellyfin.Server.Implementations/Devices/DeviceManager.cs
  85. 43 0
      Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
  86. 5 4
      Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj
  87. 0 51
      Jellyfin.Server.Implementations/JellyfinDbProvider.cs
  88. 657 0
      Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs
  89. 28 0
      Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs
  90. 18 14
      Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs
  91. 17 0
      Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs
  92. 38 32
      Jellyfin.Server.Implementations/Security/AuthenticationManager.cs
  93. 73 69
      Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
  94. 3 3
      Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs
  95. 162 131
      Jellyfin.Server.Implementations/Users/UserManager.cs
  96. 1 8
      Jellyfin.Server/CoreAppHost.cs
  97. 9 5
      Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
  98. 11 0
      Jellyfin.Server/Filters/AdditionalModelFilter.cs
  99. 4 4
      Jellyfin.Server/Jellyfin.Server.csproj
  100. 3 2
      Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

+ 0 - 15
.github/dependabot.yml

@@ -1,15 +0,0 @@
-version: 2
-updates:
-- package-ecosystem: nuget
-  directory: "/"
-  schedule:
-    interval: weekly
-    time: '12:00'
-  open-pull-requests-limit: 10
-
-- package-ecosystem: github-actions
-  directory: '/'
-  schedule:
-    interval: weekly
-    time: '12:00'
-  open-pull-requests-limit: 10

+ 6 - 0
.github/renovate.json

@@ -0,0 +1,6 @@
+{
+  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+  "extends": [
+    "github>jellyfin/.github//renovate-presets/dotnet"
+  ]
+}

+ 6 - 6
.github/workflows/automation.yml

@@ -14,7 +14,7 @@ jobs:
     if: ${{ github.repository == 'jellyfin/jellyfin' }}
     steps:
       - name: Apply label
-        uses: eps1lon/actions-label-merge-conflict@v2.0.1
+        uses: eps1lon/actions-label-merge-conflict@fd1f295ee7443d13745804bc49fe158e240f6c6e # tag=v2.1.0
         if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
         with:
           dirtyLabel: 'merge conflict'
@@ -26,7 +26,7 @@ jobs:
     if: ${{ github.repository == 'jellyfin/jellyfin' }}
     steps:
       - name: Remove from 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.8.1
+        uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         continue-on-error: true
         with:
@@ -35,7 +35,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add to 'Release Next' project
-        uses: alex-page/github-project-automation-plus@v0.8.1
+        uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
         if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
         continue-on-error: true
         with:
@@ -44,7 +44,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add to 'Current Release' project
-        uses: alex-page/github-project-automation-plus@v0.8.1
+        uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
         if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
         continue-on-error: true
         with:
@@ -58,7 +58,7 @@ jobs:
         run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)"
 
       - name: Move issue to needs triage
-        uses: alex-page/github-project-automation-plus@v0.8.1
+        uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
         if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
         continue-on-error: true
         with:
@@ -67,7 +67,7 @@ jobs:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
 
       - name: Add issue to triage project
-        uses: alex-page/github-project-automation-plus@v0.8.1
+        uses: alex-page/github-project-automation-plus@1f8873e97e3c8f58161a323b7c568c1f623a1c4d # tag=v0.8.2
         if: github.event.issue.pull_request == '' && github.event.action == 'opened'
         continue-on-error: true
         with:

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

@@ -20,18 +20,18 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
     - name: Setup .NET Core
-      uses: actions/setup-dotnet@v3
+      uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
       with:
         dotnet-version: '6.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@312e093a1892bd801f026f1090904ee8e460b9b6 # v2
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@312e093a1892bd801f026f1090904ee8e460b9b6 # v2
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@312e093a1892bd801f026f1090904ee8e460b9b6 # v2

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

@@ -16,20 +16,20 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Notify as seen
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           comment-id: ${{ github.event.comment.id }}
           reactions: '+1'
 
       - name: Checkout the latest code
-        uses: actions/checkout@v3
+        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
 
       - name: Automatic Rebase
-        uses: cirrus-actions/rebase@1.7
+        uses: cirrus-actions/rebase@6e572f08c244e2f04f9beb85a943eb618218714d # tag=1.7
         env:
           GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
 
@@ -39,7 +39,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Notify as seen
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ github.event.comment != null }}
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -47,14 +47,14 @@ jobs:
           reactions: eyes
 
       - name: Checkout the latest code
-        uses: actions/checkout@v3
+        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
 
       - name: Notify as running
         id: comment_running
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ github.event.comment != null }}
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -89,7 +89,7 @@ jobs:
           exit ${retcode}
 
       - name: Notify with result success
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ github.event.comment != null && success() }}
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -104,7 +104,7 @@ jobs:
           reactions: hooray
 
       - name: Notify with result failure
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ github.event.comment != null && failure() }}
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}

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

@@ -12,18 +12,18 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Setup .NET Core
-        uses: actions/setup-dotnet@v3
+        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
         with:
           dotnet-version: '6.0.x'
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
         with:
           name: openapi-head
           retention-days: 14
@@ -37,17 +37,17 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v3
+        uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # tag=v3
         with:
           ref: ${{ github.base_ref }}
       - name: Setup .NET Core
-        uses: actions/setup-dotnet@v3
+        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
         with:
           dotnet-version: '6.0.x'
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@83fd05a356d7e2593de66fc9913b3002723633cb # tag=v3
         with:
           name: openapi-base
           retention-days: 14
@@ -63,12 +63,12 @@ jobs:
       - openapi-base
     steps:
       - name: Download openapi-head
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
         with:
           name: openapi-head
           path: openapi-head
       - name: Download openapi-base
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # tag=v3
         with:
           name: openapi-base
           path: openapi-base
@@ -90,14 +90,14 @@ jobs:
           body="${body//$'\r'/'%0D'}"
           echo ::set-output name=body::$body
       - name: Find difference comment
-        uses: peter-evans/find-comment@v2
+        uses: peter-evans/find-comment@b657a70ff16d17651703a84bee1cb9ad9d2be2ea # tag=v2
         id: find-comment
         with:
           issue-number: ${{ github.event.pull_request.number }}
           direction: last
           body-includes: openapi-diff-workflow-comment
       - name: Reply or edit difference comment (changed)
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ steps.read-diff.outputs.body != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}
@@ -112,7 +112,7 @@ jobs:
 
             </details>
       - name: Edit difference comment (unchanged)
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
         if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}

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

@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
-      - uses: actions/stale@v6
+      - uses: actions/stale@5ebf00ea0e4c1561e9b43a292ed34424fb1d4578 # tag=v6
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           days-before-stale: 120

+ 0 - 3
.gitignore

@@ -150,8 +150,6 @@ publish/
 *.pubxml
 
 # NuGet Packages Directory
-## TODO: If you have NuGet Package Restore enabled, uncomment the next line
-# packages/
 dlls/
 dllssigned/
 
@@ -166,7 +164,6 @@ AppPackages/
 sql/
 *.Cache
 ClientBin/
-[Ss]tyle[Cc]op.*
 ~$*
 *~
 *.dbmdl

+ 1 - 0
Directory.Build.props

@@ -16,6 +16,7 @@
 
   <ItemGroup>
     <AdditionalFiles Include="$(SolutionDir)/BannedSymbols.txt" />
+    <AdditionalFiles Include="$(SolutionDir)/stylecop.json" />
   </ItemGroup>
 
 </Project>

+ 1 - 1
Dockerfile

@@ -89,4 +89,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 1 - 1
Dockerfile.arm

@@ -78,4 +78,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
     "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 1 - 1
Dockerfile.arm64

@@ -72,4 +72,4 @@ ENTRYPOINT ["./jellyfin/jellyfin", \
     "--ffmpeg", "/usr/bin/ffmpeg"]
 
 HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \
-     CMD curl -Lk "${HEALTHCHECK_URL}" || exit 1
+     CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1

+ 1 - 2
Emby.Dlna/Eventing/DlnaEventManager.cs

@@ -127,8 +127,7 @@ namespace Emby.Dlna.Eventing
         public Task TriggerEvent(string notificationType, IDictionary<string, string> stateVariables)
         {
             var subs = _subscriptions.Values
-                .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase))
-                .ToList();
+                .Where(i => !i.IsExpired && string.Equals(notificationType, i.NotificationType, StringComparison.OrdinalIgnoreCase));
 
             var tasks = subs.Select(i => TriggerEvent(i, stateVariables));
 

+ 0 - 1
Emby.Dlna/PlayTo/PlayToController.cs

@@ -338,7 +338,6 @@ namespace Emby.Dlna.PlayTo
                 SubtitleStreamIndex = info.SubtitleStreamIndex,
                 VolumeLevel = _device.Volume,
 
-                // TODO
                 CanSeek = true,
 
                 PlayMethod = info.IsDirectStream ? PlayMethod.DirectStream : PlayMethod.Transcode

+ 1 - 2
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -36,8 +36,7 @@ namespace Emby.Naming.AudioBook
             // File with empty fullname will be sorted out here.
             var audiobookFileInfos = files
                 .Select(i => _audioBookResolver.Resolve(i.FullName))
-                .OfType<AudioBookFileInfo>()
-                .ToList();
+                .OfType<AudioBookFileInfo>();
 
             var stackResult = StackResolver.ResolveAudioBooks(audiobookFileInfos);
 

+ 11 - 4
Emby.Naming/Common/NamingOptions.cs

@@ -175,6 +175,7 @@ namespace Emby.Naming.Common
             AlbumStackingPrefixes = new[]
             {
                 "cd",
+                "digital media",
                 "disc",
                 "disk",
                 "vol",
@@ -512,13 +513,13 @@ namespace Emby.Naming.Common
                     MediaType.Video),
 
                 new ExtraRule(
-                    ExtraType.Clip,
+                    ExtraType.Short,
                     ExtraRuleType.DirectoryName,
                     "shorts",
                     MediaType.Video),
 
                 new ExtraRule(
-                    ExtraType.Clip,
+                    ExtraType.Featurette,
                     ExtraRuleType.DirectoryName,
                     "featurettes",
                     MediaType.Video),
@@ -535,6 +536,12 @@ namespace Emby.Naming.Common
                     "other",
                     MediaType.Video),
 
+                new ExtraRule(
+                    ExtraType.Clip,
+                    ExtraRuleType.DirectoryName,
+                    "clips",
+                    MediaType.Video),
+
                 new ExtraRule(
                     ExtraType.Trailer,
                     ExtraRuleType.Filename,
@@ -638,13 +645,13 @@ namespace Emby.Naming.Common
                     MediaType.Video),
 
                 new ExtraRule(
-                    ExtraType.Clip,
+                    ExtraType.Featurette,
                     ExtraRuleType.Suffix,
                     "-featurette",
                     MediaType.Video),
 
                 new ExtraRule(
-                    ExtraType.Clip,
+                    ExtraType.Short,
                     ExtraRuleType.Suffix,
                     "-short",
                     MediaType.Video),

+ 1 - 2
Emby.Notifications/NotificationManager.cs

@@ -88,8 +88,7 @@ namespace Emby.Notifications
             string description,
             CancellationToken cancellationToken)
         {
-            users = users.Where(i => IsEnabledForUser(service, i))
-                .ToList();
+            users = users.Where(i => IsEnabledForUser(service, i));
 
             var tasks = users.Select(i => SendNotification(request, service, title, description, i, cancellationToken));
 

+ 14 - 9
Emby.Server.Implementations/ApplicationHost.cs

@@ -48,6 +48,7 @@ using Jellyfin.Api.Helpers;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.Networking.Configuration;
 using Jellyfin.Networking.Manager;
+using Jellyfin.Server.Implementations;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
@@ -101,6 +102,7 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
@@ -652,6 +654,17 @@ namespace Emby.Server.Implementations
         /// <returns>A task representing the service initialization operation.</returns>
         public async Task InitializeServices()
         {
+            var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDb>>().CreateDbContextAsync().ConfigureAwait(false);
+            await using (jellyfinDb.ConfigureAwait(false))
+            {
+                if ((await jellyfinDb.Database.GetPendingMigrationsAsync().ConfigureAwait(false)).Any())
+                {
+                    Logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
+                    await jellyfinDb.Database.MigrateAsync().ConfigureAwait(false);
+                    Logger.LogInformation("EFCore migrations applied successfully");
+                }
+            }
+
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 
@@ -1088,15 +1101,7 @@ namespace Emby.Server.Implementations
                 return GetLocalApiUrl(request.Host.Host, request.Scheme, requestPort);
             }
 
-            // Published server ends with a /
-            if (!string.IsNullOrEmpty(PublishedServerUrl))
-            {
-                // Published server ends with a '/', so we need to remove it.
-                return PublishedServerUrl.Trim('/');
-            }
-
-            string smart = NetManager.GetBindInterface(request, out var port);
-            return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
+            return GetSmartApiUrl(request.HttpContext.Connection.RemoteIpAddress ?? IPAddress.Loopback);
         }
 
         /// <inheritdoc/>

+ 4 - 4
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -232,10 +232,10 @@ namespace Emby.Server.Implementations.Collections
 
             if (list.Count > 0)
             {
-                var newList = collection.LinkedChildren.ToList();
-                newList.AddRange(list);
-                collection.LinkedChildren = newList.ToArray();
-
+                LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
+                collection.LinkedChildren.CopyTo(newChildren, 0);
+                list.CopyTo(newChildren, collection.LinkedChildren.Length);
+                collection.LinkedChildren = newChildren;
                 collection.UpdateRatingToItems(linkedChildrenList);
 
                 await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).ConfigureAwait(false);

+ 7 - 0
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -3524,6 +3524,13 @@ namespace Emby.Server.Implementations.Data
                 statement?.TryBind("@MinIndexNumber", query.MinIndexNumber.Value);
             }
 
+            if (query.MinParentAndIndexNumber.HasValue)
+            {
+                whereClauses.Add("((ParentIndexNumber=@MinParentAndIndexNumberParent and IndexNumber>=@MinParentAndIndexNumberIndex) or ParentIndexNumber>@MinParentAndIndexNumberParent)");
+                statement?.TryBind("@MinParentAndIndexNumberParent", query.MinParentAndIndexNumber.Value.ParentIndexNumber);
+                statement?.TryBind("@MinParentAndIndexNumberIndex", query.MinParentAndIndexNumber.Value.IndexNumber);
+            }
+
             if (query.MinDateCreated.HasValue)
             {
                 whereClauses.Add("DateCreated>=@MinDateCreated");

+ 4 - 4
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -25,13 +25,13 @@
   <ItemGroup>
     <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
     <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
-    <PackageReference Include="Mono.Nat" Version="3.0.3" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.4" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
+    <PackageReference Include="Mono.Nat" Version="3.0.4" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.3" />
   </ItemGroup>

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

@@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             }
 
-            var collectionFolders = _libraryManager.GetCollectionFolders(item).ToList();
+            var collectionFolders = _libraryManager.GetCollectionFolders(item);
 
             foreach (var collectionFolder in collectionFolders)
             {

+ 4 - 16
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -79,14 +79,6 @@ namespace Emby.Server.Implementations.IO
             TemporarilyIgnore(path);
         }
 
-        public bool IsPathLocked(string path)
-        {
-            // This method is not used by the core but it used by auto-organize
-
-            var lockedPaths = _tempIgnoredPaths.Keys.ToList();
-            return lockedPaths.Any(i => _fileSystem.AreEqual(i, path) || _fileSystem.ContainsSubPath(i, path));
-        }
-
         public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
         {
             if (string.IsNullOrEmpty(path))
@@ -145,8 +137,7 @@ namespace Emby.Server.Implementations.IO
                 .OfType<Folder>()
                 .SelectMany(f => f.PhysicalLocations)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
-                .OrderBy(i => i)
-                .ToList();
+                .OrderBy(i => i);
 
             foreach (var path in paths)
             {
@@ -372,11 +363,8 @@ namespace Emby.Server.Implementations.IO
 
             var monitorPath = !IgnorePatterns.ShouldIgnore(path);
 
-            // Ignore certain files
-            var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
-
-            // If the parent of an ignored path has a change event, ignore that too
-            if (tempIgnorePaths.Any(i =>
+            // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
+            if (_tempIgnoredPaths.Keys.Any(i =>
             {
                 if (_fileSystem.AreEqual(i, path))
                 {
@@ -491,7 +479,7 @@ namespace Emby.Server.Implementations.IO
         {
             lock (_activeRefreshers)
             {
-                foreach (var refresher in _activeRefreshers.ToList())
+                foreach (var refresher in _activeRefreshers)
                 {
                     refresher.Completed -= OnNewRefresherCompleted;
                     refresher.Dispose();

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

@@ -2590,9 +2590,9 @@ namespace Emby.Server.Implementations.Library
                 {
                     /*
                     Anime series don't generally have a season in their file name, however,
-                    tvdb needs a season to correctly get the metadata.
+                    TVDb needs a season to correctly get the metadata.
                     Hence, a null season needs to be filled with something. */
-                    // FIXME perhaps this would be better for tvdb parser to ask for season 1 if no season is specified
+                    // FIXME perhaps this would be better for TVDb parser to ask for season 1 if no season is specified
                     episode.ParentIndexNumber = 1;
                 }
 

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs

@@ -376,7 +376,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
                 if (!justName.IsEmpty)
                 {
-                    // check for tmdb id
+                    // Check for TMDb id
                     var tmdbid = justName.GetAttributeValue("tmdbid");
 
                     if (!string.IsNullOrWhiteSpace(tmdbid))
@@ -387,7 +387,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
                 if (!string.IsNullOrEmpty(item.Path))
                 {
-                    // check for imdb id - we use full media path, as we can assume, that this will match in any use case (either id in parent dir or in file name)
+                    // Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether  id in parent dir or in file name)
                     var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
 
                     if (!string.IsNullOrWhiteSpace(imdbid))

+ 11 - 9
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -31,16 +31,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
             if (args.IsDirectory)
             {
                 // It's a boxset if the path is a directory with [playlist] in it's the name
-                // TODO: Should this use Path.GetDirectoryName() instead?
-                bool isBoxSet = Path.GetFileName(args.Path)
-                    ?.Contains("[playlist]", StringComparison.OrdinalIgnoreCase)
-                    ?? false;
-                if (isBoxSet)
+                var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
+                if (string.IsNullOrEmpty(filename))
+                {
+                    return null;
+                }
+
+                if (filename.Contains("[playlist]", StringComparison.OrdinalIgnoreCase))
                 {
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = Path.GetFileName(args.Path).Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                        Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
                     };
                 }
 
@@ -51,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = Path.GetFileName(args.Path)
+                        Name = filename
                     };
                 }
             }
@@ -60,8 +62,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
             // It should have the correct collection type and a supported file extension
             else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
             {
-                var extension = Path.GetExtension(args.Path);
-                if (Playlist.SupportedExtensions.Contains(extension ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+                var extension = Path.GetExtension(args.Path.AsSpan());
+                if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
                 {
                     return new Playlist
                     {

+ 10 - 37
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -2192,16 +2192,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         private void HandleDuplicateShowIds(List<TimerInfo> timers)
         {
-            foreach (var timer in timers.Skip(1))
+            // sort showings by HD channels first, then by startDate, record earliest showing possible
+            foreach (var timer in timers.OrderByDescending(t => _liveTvManager.GetLiveTvChannel(t, this).IsHD).ThenBy(t => t.StartDate).Skip(1))
             {
-                // TODO: Get smarter, prefer HD, etc
-
                 timer.Status = RecordingStatus.Cancelled;
                 _timerProvider.Update(timer);
             }
         }
 
-        private void SearchForDuplicateShowIds(List<TimerInfo> timers)
+        private void SearchForDuplicateShowIds(IEnumerable<TimerInfo> timers)
         {
             var groups = timers.ToLookup(i => i.ShowId ?? string.Empty).ToList();
 
@@ -2282,39 +2281,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                     if (updateTimerSettings)
                     {
-                        // Only update if not currently active - test both new timer and existing in case Id's are different
-                        // Id's could be different if the timer was created manually prior to series timer creation
-                        if (!_activeRecordings.TryGetValue(timer.Id, out _) && !_activeRecordings.TryGetValue(existingTimer.Id, out _))
-                        {
-                            UpdateExistingTimerWithNewMetadata(existingTimer, timer);
-
-                            // Needed by ShouldCancelTimerForSeriesTimer
-                            timer.IsManual = existingTimer.IsManual;
-
-                            if (ShouldCancelTimerForSeriesTimer(seriesTimer, timer))
-                            {
-                                existingTimer.Status = RecordingStatus.Cancelled;
-                            }
-                            else if (!existingTimer.IsManual)
-                            {
-                                existingTimer.Status = RecordingStatus.New;
-                            }
-
-                            if (existingTimer.Status != RecordingStatus.Cancelled)
-                            {
-                                enabledTimersForSeries.Add(existingTimer);
-                            }
-
-                            existingTimer.KeepUntil = seriesTimer.KeepUntil;
-                            existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
-                            existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
-                            existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
-                            existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
-                            existingTimer.Priority = seriesTimer.Priority;
-                            existingTimer.SeriesTimerId = seriesTimer.Id;
-
-                            _timerProvider.Update(existingTimer);
-                        }
+                        existingTimer.KeepUntil = seriesTimer.KeepUntil;
+                        existingTimer.IsPostPaddingRequired = seriesTimer.IsPostPaddingRequired;
+                        existingTimer.IsPrePaddingRequired = seriesTimer.IsPrePaddingRequired;
+                        existingTimer.PostPaddingSeconds = seriesTimer.PostPaddingSeconds;
+                        existingTimer.PrePaddingSeconds = seriesTimer.PrePaddingSeconds;
+                        existingTimer.Priority = seriesTimer.Priority;
+                        existingTimer.SeriesTimerId = seriesTimer.Id;
                     }
 
                     existingTimer.SeriesTimerId = seriesTimer.Id;

+ 20 - 3
Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs

@@ -122,11 +122,28 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
             if (_timers.TryAdd(item.Id, timer))
             {
-                Logger.LogInformation(
-                    "Creating recording timer for {Id}, {Name}. Timer will fire in {Minutes} minutes",
+                if (item.IsSeries)
+                {
+                    Logger.LogInformation(
+                    "Creating recording timer for {Id}, {Name} {SeasonNumber}x{EpisodeNumber:D2} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}",
                     item.Id,
                     item.Name,
-                    dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+                    item.SeasonNumber,
+                    item.EpisodeNumber,
+                    item.ChannelId,
+                    dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture),
+                    item.StartDate);
+                }
+                else
+                {
+                    Logger.LogInformation(
+                    "Creating recording timer for {Id}, {Name} on channel {ChannelId}. Timer will fire in {Minutes} minutes at {StartDate}",
+                    item.Id,
+                    item.Name,
+                    item.ChannelId,
+                    dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture),
+                    item.StartDate);
+                }
             }
             else
             {

+ 9 - 6
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -166,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
                         const double DesiredAspect = 2.0 / 3;
 
-                        programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
-                                                    GetProgramImage(ApiUrl, allImages, DesiredAspect);
+                        programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect, token) ??
+                                                    GetProgramImage(ApiUrl, allImages, DesiredAspect, token);
 
                         const double WideAspect = 16.0 / 9;
 
-                        programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
+                        programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect, token);
 
                         // Don't supply the same image twice
                         if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
@@ -179,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                             programEntry.ThumbImage = null;
                         }
 
-                        programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
+                        programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect, token);
 
                         // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
                         //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return info;
         }
 
-        private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
+        private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect, string token)
         {
             var match = images
                 .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
@@ -424,7 +424,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
             else
             {
-                return apiUrl + "/image/" + uri;
+                return apiUrl + "/image/" + uri + "?token=" + token;
             }
         }
 
@@ -458,6 +458,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             IReadOnlyList<string> programIds,
             CancellationToken cancellationToken)
         {
+            var token = await GetToken(info, cancellationToken).ConfigureAwait(false);
+
             if (programIds.Count == 0)
             {
                 return Array.Empty<ShowImagesDto>();
@@ -479,6 +481,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
                 Content = new StringContent(str.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json)
             };
+            message.Headers.TryAddWithoutValidation("token", token);
 
             try
             {

+ 2 - 5
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -32,18 +32,15 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly IServerConfigurationManager _config;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly ILogger<XmlTvListingsProvider> _logger;
-        private readonly IFileSystem _fileSystem;
 
         public XmlTvListingsProvider(
             IServerConfigurationManager config,
             IHttpClientFactory httpClientFactory,
-            ILogger<XmlTvListingsProvider> logger,
-            IFileSystem fileSystem)
+            ILogger<XmlTvListingsProvider> logger)
         {
             _config = config;
             _httpClientFactory = httpClientFactory;
             _logger = logger;
-            _fileSystem = fileSystem;
         }
 
         public string Name => "XmlTV";
@@ -165,7 +162,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
                 OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
                 CommunityRating = program.StarRating,
-                SeriesId = program.Episode == null ? null : program.Title.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+                SeriesId = program.Episode == null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             if (string.IsNullOrWhiteSpace(program.ProgramId))

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

@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
                 int receivedBytes = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
 
-                return VerifyReturnValueOfGetSet(buffer.AsSpan(receivedBytes), "none");
+                return VerifyReturnValueOfGetSet(buffer.AsSpan(0, receivedBytes), "none");
             }
             finally
             {

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

@@ -97,7 +97,7 @@
     "TasksChannelsCategory": "قنوات الإنترنت",
     "TasksLibraryCategory": "مكتبة",
     "TasksMaintenanceCategory": "صيانة",
-    "TaskRefreshLibraryDescription": "يفصح مكتبة الوسائط الخاصة بك بحثًا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
+    "TaskRefreshLibraryDescription": "يفحص مكتبة الوسائط الخاصة بك باحثا عن ملفات جديدة، ومن ثم يتحدث البيانات الوصفية.",
     "TaskRefreshLibrary": "افحص مكتبة الوسائط",
     "TaskRefreshChapterImagesDescription": "يُنشئ صور مصغرة لمقاطع الفيديو التي تحتوي على فصول.",
     "TaskRefreshChapterImages": "استخراج صور الفصل",

+ 2 - 1
Emby.Server.Implementations/Localization/Core/ca.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimitzar la base de dades",
     "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
     "TaskKeyframeExtractor": "Extractor de fotogrames clau",
-    "External": "Extern"
+    "External": "Extern",
+    "HearingImpaired": "Discapacitat Auditiva"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/cs.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimalizovat databázi",
     "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
     "TaskKeyframeExtractor": "Vytahovač klíčových snímků",
-    "External": "Externí"
+    "External": "Externí",
+    "HearingImpaired": "Sluchově postižení"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/da.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimér database",
     "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.",
     "TaskKeyframeExtractor": "Billedramme udtrækker",
-    "External": "Ekstern"
+    "External": "Ekstern",
+    "HearingImpaired": "Hørehæmmet"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/de.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Datenbank optimieren",
     "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
     "TaskKeyframeExtractor": "Keyframe Extraktor",
-    "External": "Extern"
+    "External": "Extern",
+    "HearingImpaired": "Hörgeschädigt"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/el.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων",
     "TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
     "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
-    "External": "Εξωτερικό"
+    "External": "Εξωτερικό",
+    "HearingImpaired": "Με προβλήματα ακοής"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/en-GB.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimise database",
     "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
     "TaskKeyframeExtractor": "Keyframe Extractor",
-    "External": "External"
+    "External": "External",
+    "HearingImpaired": "Hearing Impaired"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/es-AR.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimización de base de datos",
     "External": "Externo",
     "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
-    "TaskKeyframeExtractor": "Extractor de Fotogramas Clave"
+    "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
+    "HearingImpaired": "Personas con discapacidad auditiva"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/es-MX.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.",
     "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.",
     "TaskKeyframeExtractor": "Extractor de Cuadros Clave",
-    "External": "Externo"
+    "External": "Externo",
+    "HearingImpaired": "Discapacidad Auditiva"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/es.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabaseDescription": "Optimiza y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
     "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
     "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
-    "External": "Externo"
+    "External": "Externo",
+    "HearingImpaired": "Discapacidad Auditiva"
 }

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

@@ -120,5 +120,8 @@
     "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
     "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
     "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}",
-    "External": "Väline"
+    "External": "Väline",
+    "HearingImpaired": "Kuulmispuudega",
+    "TaskKeyframeExtractorDescription": "Eraldab videofailidest võtmekaadreid, et luua täpsemaid HLS-i esitusloendeid. See ülesanne võib kesta pikka aega.",
+    "TaskKeyframeExtractor": "Võtmekaadri ekstraktor"
 }

+ 8 - 1
Emby.Server.Implementations/Localization/Core/eu.json

@@ -116,5 +116,12 @@
     "CameraImageUploadedFrom": "{0}-tik kamera irudi berri bat igo da",
     "AuthenticationSucceededWithUserName": "{0} ongi autentifikatu da",
     "Application": "Aplikazioa",
-    "AppDeviceValues": "App: {0}, Gailua: {1}"
+    "AppDeviceValues": "App: {0}, Gailua: {1}",
+    "HearingImpaired": "Entzunaldia aldatua",
+    "ProviderValue": "Hornitzailea: {0}",
+    "TaskKeyframeExtractorDescription": "Bideo fitxategietako fotograma gakoak ateratzen ditu HLS erreprodukzio-zerrenda zehatzagoak sortzeko. Zeregin honek denbora asko iraun dezake.",
+    "HeaderRecordingGroups": "Grabaketa taldeak",
+    "Inherit": "Oinordetu",
+    "TaskOptimizeDatabaseDescription": "Datu-basea trinkotu eta bertatik espazioa askatzen du. Liburutegia eskaneatu ondoren edo datu-basean aldaketak egin ondoren ataza hau exekutatzeak errendimendua hobetu lezake.",
+    "TaskKeyframeExtractor": "Fotograma gakoen erauzgailua"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/fi.json

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "Optimoi tietokanta",
     "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
     "TaskKeyframeExtractor": "Avainkuvien purkain",
-    "External": "Ulkoinen"
+    "External": "Ulkoinen",
+    "HearingImpaired": "Kuulorajoitteinen"
 }

+ 3 - 2
Emby.Server.Implementations/Localization/Core/fr-CA.json

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une nouvelle image de caméra a été téléchargée depuis {0}",
+    "CameraImageUploadedFrom": "Une nouvelle photo a été téléversée depuis {0}",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimiser la base de données",
     "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
     "TaskKeyframeExtractor": "Extracteur d'image clé",
-    "External": "Externe"
+    "External": "Externe",
+    "HearingImpaired": "Malentendants"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/fr.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimiser la base de données",
     "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
     "TaskKeyframeExtractor": "Extracteur d'image clé",
-    "External": "Externe"
+    "External": "Externe",
+    "HearingImpaired": "Malentendants"
 }

+ 6 - 2
Emby.Server.Implementations/Localization/Core/gl.json

@@ -47,7 +47,7 @@
     "HeaderFavoriteEpisodes": "Episodios Favoritos",
     "HeaderFavoriteArtists": "Artistas Favoritos",
     "HeaderFavoriteAlbums": "Álbunes Favoritos",
-    "HeaderContinueWatching": "Seguir mirando",
+    "HeaderContinueWatching": "Seguir vendo",
     "HeaderAlbumArtists": "Artistas do Album",
     "Genres": "Xéneros",
     "Forced": "Forzado",
@@ -119,5 +119,9 @@
     "UserOnlineFromDevice": "{0} está en liña desde {1}",
     "UserOfflineFromDevice": "{0} desconectouse desde {1}",
     "TaskOptimizeDatabaseDescription": "Compacta e libera o espazo libre da base de datos. Executar esta tarefa logo de realizar mudanzas que impliquen modificacións da base de datos ou despois de escanear a biblioteca pode traer mellorías de desempeño.",
-    "TaskOptimizeDatabase": "Optimizar base de datos"
+    "TaskOptimizeDatabase": "Optimizar base de datos",
+    "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
+    "External": "Externo",
+    "HearingImpaired": "Problemas de audición",
+    "TaskKeyframeExtractor": "Extractor de fragmentos"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/he.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים.",
     "TaskKeyframeExtractorDescription": "חלץ תמונות מפתח מקבצי וידאו בכדי ליצור רשימות השמעה מדויקות יותר של HLS. משימה זו עלולה להימשך זמן רב.",
     "TaskKeyframeExtractor": "מחלץ תמונות מפתח",
-    "External": "חיצוני"
+    "External": "חיצוני",
+    "HearingImpaired": "לקוי שמיעה"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/hr.json

@@ -123,5 +123,6 @@
     "External": "Vanjski",
     "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
     "TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
-    "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka."
+    "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
+    "HearingImpaired": "Oštećen sluh"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/hu.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Adatbázis optimalizálása",
     "TaskKeyframeExtractor": "Kulcskockák kibontása",
     "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
-    "External": "Külső"
+    "External": "Külső",
+    "HearingImpaired": "Hallássérült"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/id.json

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "Optimalkan basis data",
     "TaskKeyframeExtractorDescription": "Ekstrak bingkai utama dari file video untuk membuat daftar putar HLS yang lebih tepat. Tugas ini dapat berjalan untuk waktu yang lama.",
     "TaskKeyframeExtractor": "Ekstraktor Bingkai Utama",
-    "External": "Luar"
+    "External": "Luar",
+    "HearingImpaired": "Gangguan Pendengaran"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/it.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Ottimizza Database",
     "TaskKeyframeExtractor": "Estrattore di Keyframe",
     "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
-    "External": "Esterno"
+    "External": "Esterno",
+    "HearingImpaired": "con problemi di udito"
 }

+ 7 - 0
Emby.Server.Implementations/Localization/Core/jbo.json

@@ -0,0 +1,7 @@
+{
+    "Albums": "lo albuma",
+    "Artists": "lo larpra",
+    "Books": "lo cukta",
+    "HeaderAlbumArtists": "lo albuma larpra",
+    "Playlists": "lo zgipor"
+}

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

@@ -0,0 +1,3 @@
+{
+    "Albums": "Albums"
+}

+ 2 - 1
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -123,5 +123,6 @@
     "TaskKeyframeExtractorDescription": "Iš vaizdo įrašo paruošia reikšminius kadrus, kad būtų sukuriamas tikslenis HLS grojaraštis. Šios užduoties vykdymas gali ilgai užtrukti.",
     "TaskKeyframeExtractor": "Pagrindinių kadrų ištraukėjas",
     "TaskOptimizeDatabaseDescription": "Suspaudžia duomenų bazę ir atlaisvina vietą. Paleidžiant šią užduotį, po bibliotekos skenavimo arba kitų veiksmų kurie galimai modifikuoja duomenų bazė, gali pagerinti greitaveiką.",
-    "External": "Išorinis"
+    "External": "Išorinis",
+    "HearingImpaired": "Su klausos sutrikimais"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/nb.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabaseDescription": "Komprimerer database og frigjør plass. Denne prosessen kan forbedre ytelsen etter skanning av bibliotek eller andre handlinger som fører til databaseendringer.",
     "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.",
     "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker",
-    "External": "Ekstern"
+    "External": "Ekstern",
+    "HearingImpaired": "Hørselshemmet"
 }

+ 4 - 3
Emby.Server.Implementations/Localization/Core/nl.json

@@ -5,7 +5,7 @@
     "Artists": "Artiesten",
     "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
     "Books": "Boeken",
-    "CameraImageUploadedFrom": "Nieuwe camera afbeelding toegevoegd vanaf {0}",
+    "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
     "Channels": "Kanalen",
     "ChapterNameValue": "Hoofdstuk {0}",
     "Collections": "Verzamelingen",
@@ -15,7 +15,7 @@
     "Favorites": "Favorieten",
     "Folders": "Mappen",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Album Artiesten",
+    "HeaderAlbumArtists": "Albumartiesten",
     "HeaderContinueWatching": "Kijken hervatten",
     "HeaderFavoriteAlbums": "Favoriete albums",
     "HeaderFavoriteArtists": "Favoriete artiesten",
@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Database optimaliseren",
     "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS afspeellijsten te maken. Dit kan lang duren.",
     "TaskKeyframeExtractor": "Keyframe Extractor",
-    "External": "Extern"
+    "External": "Extern",
+    "HearingImpaired": "Slechthorend"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/pt-BR.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Otimizar base de dados",
     "TaskKeyframeExtractor": "Extrator de quadro-chave",
     "TaskKeyframeExtractorDescription": "Extrai quadros-chave de arquivos de vídeo para criar listas de reprodução HLS mais precisas. Esta tarefa pode ser executada por um longo tempo.",
-    "External": "Externo"
+    "External": "Externo",
+    "HearingImpaired": "Deficiência Auditiva"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/pt-PT.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Otimizar base de dados",
     "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
     "TaskKeyframeExtractor": "Extrator de Quadros-chave",
-    "External": "Externo"
+    "External": "Externo",
+    "HearingImpaired": "Surdo"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/pt.json

@@ -120,5 +120,6 @@
     "TaskCleanActivityLogDescription": "Apaga itens no registro com idade acima do que é configurado.",
     "TaskOptimizeDatabase": "Otimizar base de dados",
     "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
-    "External": "Externo"
+    "External": "Externo",
+    "HearingImpaired": "Problemas auditivos"
 }

+ 24 - 23
Emby.Server.Implementations/Localization/Core/ro.json

@@ -11,7 +11,7 @@
     "UserOfflineFromDevice": "{0} s-a deconectat de la {1}",
     "UserLockedOutWithName": "Utilizatorul {0} a fost blocat",
     "UserDownloadingItemWithValues": "{0} descarcă {1}",
-    "UserDeletedWithName": "Utilizatorul {0} a fost eliminat",
+    "UserDeletedWithName": "Utilizatorul {0} a fost șters",
     "UserCreatedWithName": "Utilizatorul {0} a fost creat",
     "User": "Utilizator",
     "TvShows": "Seriale TV",
@@ -20,33 +20,33 @@
     "SubtitleDownloadFailureFromForItem": "Subtitrările nu au putut fi descărcate de la {0} pentru {1}",
     "StartupEmbyServerIsLoading": "Se încarcă serverul Jellyfin. Încercați din nou în scurt timp.",
     "Songs": "Melodii",
-    "Shows": "Spectacole",
-    "ServerNameNeedsToBeRestarted": "{0} trebuie repornit",
+    "Shows": "Seriale",
+    "ServerNameNeedsToBeRestarted": "{0} trebuie să fie repornit",
     "ScheduledTaskStartedWithName": "{0} pornit/ă",
     "ScheduledTaskFailedWithName": "{0} eșuat/ă",
     "ProviderValue": "Furnizor: {0}",
     "PluginUpdatedWithName": "{0} a fost actualizat/ă",
     "PluginUninstalledWithName": "{0} a fost dezinstalat",
     "PluginInstalledWithName": "{0} a fost instalat",
-    "Plugin": "Plugin",
-    "Playlists": "Liste redare",
+    "Plugin": "Extensie",
+    "Playlists": "Liste de redare",
     "Photos": "Fotografii",
     "NotificationOptionVideoPlaybackStopped": "Redarea video oprită",
     "NotificationOptionVideoPlayback": "Redare video începută",
     "NotificationOptionUserLockedOut": "Utilizatorul a fost blocat",
-    "NotificationOptionTaskFailed": "Activitate programata eșuată",
+    "NotificationOptionTaskFailed": "Activitate programată eșuată",
     "NotificationOptionServerRestartRequired": "Este necesară repornirea serverului",
-    "NotificationOptionPluginUpdateInstalled": "Actualizare plugin instalată",
-    "NotificationOptionPluginUninstalled": "Plugin dezinstalat",
-    "NotificationOptionPluginInstalled": "Plugin instalat",
-    "NotificationOptionPluginError": "Plugin-ul a eșuat",
-    "NotificationOptionNewLibraryContent": "Adăugat conținut nou",
-    "NotificationOptionInstallationFailed": "Eșec la instalare",
-    "NotificationOptionCameraImageUploaded": "Încarcată imagine cameră",
+    "NotificationOptionPluginUpdateInstalled": "Actualizarea extensiei este instalată",
+    "NotificationOptionPluginUninstalled": "Extensie dezinstalată",
+    "NotificationOptionPluginInstalled": "Extensie instalată",
+    "NotificationOptionPluginError": "Eroare de extensie",
+    "NotificationOptionNewLibraryContent": "A fost adăugat conținut nou",
+    "NotificationOptionInstallationFailed": "Instalare eșuată",
+    "NotificationOptionCameraImageUploaded": "Imagine încarcată",
     "NotificationOptionAudioPlaybackStopped": "Redare audio oprită",
     "NotificationOptionAudioPlayback": "A început redarea audio",
     "NotificationOptionApplicationUpdateInstalled": "Actualizarea aplicației a fost instalată",
-    "NotificationOptionApplicationUpdateAvailable": "Disponibilă o actualizare a aplicației",
+    "NotificationOptionApplicationUpdateAvailable": "Este disponibilă o actualizare a aplicației",
     "NewVersionIsAvailable": "O nouă versiune a Jellyfin Server este disponibilă pentru descărcare.",
     "NameSeasonUnknown": "Sezon Necunoscut",
     "NameSeasonNumber": "Sezonul {0}",
@@ -54,8 +54,8 @@
     "MusicVideos": "Videoclipuri muzicale",
     "Music": "Muzică",
     "Movies": "Filme",
-    "MixedContent": "Conținut mixt",
-    "MessageServerConfigurationUpdated": "Configurația serverului a fost actualizată",
+    "MixedContent": "Conținut amestecat",
+    "MessageServerConfigurationUpdated": "Configurarea serverului a fost actualizată",
     "MessageNamedServerConfigurationUpdatedWithValue": "Secțiunea de configurare a serverului {0} a fost acualizata",
     "MessageApplicationUpdatedTo": "Jellyfin Server a fost actualizat la {0}",
     "MessageApplicationUpdated": "Jellyfin Server a fost actualizat",
@@ -69,7 +69,7 @@
     "HeaderRecordingGroups": "Grupuri de înregistrare",
     "HeaderLiveTV": "TV în Direct",
     "HeaderFavoriteSongs": "Melodii Favorite",
-    "HeaderFavoriteShows": "Spectacole Favorite",
+    "HeaderFavoriteShows": "Seriale TV Favorite",
     "HeaderFavoriteEpisodes": "Episoade Favorite",
     "HeaderFavoriteArtists": "Artiști Favoriți",
     "HeaderFavoriteAlbums": "Albume Favorite",
@@ -97,10 +97,10 @@
     "TaskRefreshChannels": "Actualizează canale",
     "TaskCleanTranscodeDescription": "Șterge fișierele de transcodare mai vechi de o zi.",
     "TaskCleanTranscode": "Curățați directorul de transcodare",
-    "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru pluginuri care sunt configurate să se actualizeze automat.",
-    "TaskUpdatePlugins": "Actualizați plugin-uri",
+    "TaskUpdatePluginsDescription": "Descarcă și instalează actualizări pentru extensiile care sunt configurate să se actualizeze automat.",
+    "TaskUpdatePlugins": "Actualizați Extensile",
     "TaskRefreshPeopleDescription": "Actualizează metadatele pentru actori și regizori din biblioteca media.",
-    "TaskRefreshPeople": "Actualizează oamenii",
+    "TaskRefreshPeople": "Actualizează Persoanele",
     "TaskCleanLogsDescription": "Șterge fișierele jurnal care au mai mult de {0} zile.",
     "TaskCleanLogs": "Curățare director jurnal",
     "TaskRefreshLibraryDescription": "Scanează biblioteca media pentru fișiere noi și reîmprospătează metadatele.",
@@ -114,13 +114,14 @@
     "TasksLibraryCategory": "Librărie",
     "TasksMaintenanceCategory": "Mentenanță",
     "TaskCleanActivityLogDescription": "Șterge intrările din jurnalul de activitate mai vechi de data configurată.",
-    "TaskCleanActivityLog": "Curăță Jurnalul de Activitate",
+    "TaskCleanActivityLog": "Curăță Jurnalul de Activități",
     "Undefined": "Nedefinit",
     "Forced": "Forțat",
     "Default": "Implicit",
-    "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.",
+    "TaskOptimizeDatabaseDescription": "Comprimă baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.",
     "TaskOptimizeDatabase": "Optimizează baza de date",
     "TaskKeyframeExtractorDescription": "Extrage cadrele cheie din fișierele video pentru a crea liste de redare HLS mai precise. Această sarcină poate rula o perioadă lungă de timp.",
     "External": "Extern",
-    "TaskKeyframeExtractor": "Extractor de cadre cheie"
+    "TaskKeyframeExtractor": "Extractor de cadre cheie",
+    "HearingImpaired": "Ascultare Impară"
 }

+ 4 - 3
Emby.Server.Implementations/Localization/Core/ru.json

@@ -75,7 +75,7 @@
     "StartupEmbyServerIsLoading": "Jellyfin Server загружается. Повторите попытку в ближайшее время.",
     "SubtitleDownloadFailureForItem": "Субтитры к {0} не удалось загрузить",
     "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
-    "Sync": "Синхро",
+    "Sync": "Синхронизация",
     "System": "Система",
     "TvShows": "ТВ",
     "User": "Пользователь",
@@ -117,11 +117,12 @@
     "TaskCleanActivityLogDescription": "Удаляет записи журнала активности старше установленного возраста.",
     "TaskCleanActivityLog": "Очистка журнала активности",
     "Undefined": "Не определено",
-    "Forced": "Форсир-ые",
+    "Forced": "Принудительно",
     "Default": "По умолчанию",
     "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
     "TaskOptimizeDatabase": "Оптимизация базы данных",
     "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
     "TaskKeyframeExtractor": "Извлечение ключевых кадров",
-    "External": "Внешние"
+    "External": "Внешние",
+    "HearingImpaired": "Для слабослышащих"
 }

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

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "Optimalizovať databázu",
     "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",
-    "External": "Externé"
+    "External": "Externé",
+    "HearingImpaired": "Sluchovo Postihnutý"
 }

+ 5 - 1
Emby.Server.Implementations/Localization/Core/sq.json

@@ -119,5 +119,9 @@
     "Forced": "I detyruar",
     "Default": "Parazgjedhur",
     "TaskOptimizeDatabaseDescription": "Kompakton bazën e të dhënave dhe shkurton hapësirën e lirë. Drejtimi i kësaj detyre pasi skanoni bibliotekën ose bëni ndryshime të tjera që nënkuptojnë modifikime të bazës së të dhënave mund të përmirësojë performancën.",
-    "TaskOptimizeDatabase": "Optimizo databazën"
+    "TaskOptimizeDatabase": "Optimizo databazën",
+    "TaskKeyframeExtractorDescription": "Nxjerrë kornizat kryesore nga skedarët video për të krijuar lista luajtjeje më të sakta HLS. Ky veprim mund të dojë një kohë të gjatë për tu kompletuar.",
+    "TaskKeyframeExtractor": "Nxjerrës i kornizës kryesore",
+    "External": "Jashtem",
+    "HearingImpaired": "Dëgjimi i dëmtuar"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/uk.json

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabaseDescription": "Стискає базу даних та збільшує вільний простір. Виконання цього завдання після сканування медіатеки або внесення інших змін, які передбачають модифікацію бази даних може покращити продуктивність.",
     "TaskKeyframeExtractorDescription": "Витягує ключові кадри з відеофайлів для створення більш точних списків відтворення HLS. Це завдання може виконуватися протягом тривалого часу.",
     "TaskKeyframeExtractor": "Екстрактор ключових кадрів",
-    "External": "Зовнішній"
+    "External": "Зовнішній",
+    "HearingImpaired": "З порушеннями слуху"
 }

+ 5 - 5
Emby.Server.Implementations/Localization/Core/ur_PK.json

@@ -5,18 +5,18 @@
     "HeaderAlbumArtists": "البم کے فنکار",
     "Movies": "فلمیں",
     "HeaderFavoriteEpisodes": "پسندیدہ اقساط",
-    "Collections": "مجموعہ",
+    "Collections": "مجموعے",
     "Folders": "فولڈرز",
     "HeaderLiveTV": "براہ راست ٹی وی",
     "Channels": "چینلز",
     "HeaderContinueWatching": "دیکھنا جاری رکھیں",
     "Playlists": "پلے لسٹس",
-    "ValueSpecialEpisodeName": "خاص - {0}",
-    "Shows": "شوز",
+    "ValueSpecialEpisodeName": "خصوصی - {0}",
+    "Shows": "دکھاتا ہے۔",
     "Genres": "انواع",
     "Artists": "فنکار",
-    "Sync": "مطابقت",
-    "Photos": "تصوریں",
+    "Sync": "مطابقت پذیری",
+    "Photos": "تصاویر",
     "Albums": "البمز",
     "Favorites": "پسندیدہ",
     "Songs": "گانے",

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

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu",
     "TaskKeyframeExtractor": "Trích Xuất Khung Hình",
     "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài.",
-    "External": "Bên ngoài"
+    "External": "Bên ngoài",
+    "HearingImpaired": "Khiếm Thính"
 }

+ 2 - 1
Emby.Server.Implementations/Localization/Core/zh-CN.json

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabase": "优化数据库",
     "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
     "TaskKeyframeExtractor": "关键帧提取器",
-    "External": "外部"
+    "External": "外部",
+    "HearingImpaired": "听力障碍"
 }

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

@@ -123,5 +123,6 @@
     "TaskCleanActivityLogDescription": "刪除早於設定時間的日誌記錄。",
     "TaskKeyframeExtractorDescription": "提取關鍵格以創建更準確的HLS播放列表。次指示可能用時很長。",
     "TaskKeyframeExtractor": "關鍵幀提取器",
-    "External": "外部"
+    "External": "外部",
+    "HearingImpaired": "聽力障礙"
 }

+ 3 - 2
Emby.Server.Implementations/Localization/Core/zh-TW.json

@@ -37,7 +37,7 @@
     "MixedContent": "混合內容",
     "Movies": "電影",
     "Music": "音樂",
-    "MusicVideos": "音樂錄影帶",
+    "MusicVideos": "MV",
     "NameInstallFailed": "{0} 安裝失敗",
     "NameSeasonNumber": "第 {0} 季",
     "NameSeasonUnknown": "未知季數",
@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "最佳化資料庫",
     "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
     "TaskKeyframeExtractor": "關鍵幀提取器",
-    "External": "外部"
+    "External": "外部",
+    "HearingImpaired": "聽力障礙"
 }

+ 3 - 2
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -386,6 +386,7 @@ namespace Emby.Server.Implementations.Localization
             yield return new LocalizationOption("Español (Dominicana)", "es_DO");
             yield return new LocalizationOption("Español (México)", "es-MX");
             yield return new LocalizationOption("Eesti", "et");
+            yield return new LocalizationOption("Basque", "eu");
             yield return new LocalizationOption("فارسی", "fa");
             yield return new LocalizationOption("Suomi", "fi");
             yield return new LocalizationOption("Filipino", "fil");
@@ -433,8 +434,8 @@ namespace Emby.Server.Implementations.Localization
             yield return new LocalizationOption("Українська", "uk");
             yield return new LocalizationOption("اُردُو", "ur_PK");
             yield return new LocalizationOption("Tiếng Việt", "vi");
-            yield return new LocalizationOption("汉语 (简字)", "zh-CN");
-            yield return new LocalizationOption("漢語 (繁字)", "zh-TW");
+            yield return new LocalizationOption("汉语 (简字)", "zh-CN");
+            yield return new LocalizationOption("漢語 (繁字)", "zh-TW");
             yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
         }
     }

+ 2 - 0
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -715,6 +715,7 @@ namespace Emby.Server.Implementations.Plugins
             {
                 // This value is memory only - so that the web will show restart required.
                 plugin.Manifest.Status = PluginStatus.Restart;
+                plugin.Manifest.AutoUpdate = false;
                 return;
             }
 
@@ -729,6 +730,7 @@ namespace Emby.Server.Implementations.Plugins
 
             // This value is memory only - so that the web will show restart required.
             plugin.Manifest.Status = PluginStatus.Restart;
+            plugin.Manifest.AutoUpdate = false;
         }
     }
 }

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

@@ -16,6 +16,7 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 {
@@ -24,15 +25,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
     /// </summary>
     public class ChapterImagesTask : IScheduledTask
     {
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
+        private readonly ILogger<ChapterImagesTask> _logger;
         private readonly ILibraryManager _libraryManager;
-
         private readonly IItemRepository _itemRepo;
-
         private readonly IApplicationPaths _appPaths;
-
         private readonly IEncodingManager _encodingManager;
         private readonly IFileSystem _fileSystem;
         private readonly ILocalizationManager _localization;
@@ -40,6 +36,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// <summary>
         /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class.
         /// </summary>
+        /// <param name="logger">The logger.</param>.
         /// <param name="libraryManager">The library manager.</param>.
         /// <param name="itemRepo">The item repository.</param>
         /// <param name="appPaths">The application paths.</param>
@@ -47,6 +44,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         /// <param name="fileSystem">The filesystem.</param>
         /// <param name="localization">The localization manager.</param>
         public ChapterImagesTask(
+            ILogger<ChapterImagesTask> logger,
             ILibraryManager libraryManager,
             IItemRepository itemRepo,
             IApplicationPaths appPaths,
@@ -54,6 +52,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             IFileSystem fileSystem,
             ILocalizationManager localization)
         {
+            _logger = logger;
             _libraryManager = libraryManager;
             _itemRepo = itemRepo;
             _appPaths = appPaths;
@@ -167,9 +166,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
 
                     progress.Report(100 * percent);
                 }
-                catch (ObjectDisposedException)
+                catch (ObjectDisposedException ex)
                 {
                     // TODO Investigate and properly fix.
+                    _logger.LogError(ex, "Object Disposed");
                     break;
                 }
             }

+ 15 - 14
Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs

@@ -17,7 +17,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
     {
         private readonly ILogger<OptimizeDatabaseTask> _logger;
         private readonly ILocalizationManager _localization;
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="OptimizeDatabaseTask" /> class.
@@ -28,7 +28,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         public OptimizeDatabaseTask(
             ILogger<OptimizeDatabaseTask> logger,
             ILocalizationManager localization,
-            JellyfinDbProvider provider)
+            IDbContextFactory<JellyfinDb> provider)
         {
             _logger = logger;
             _localization = localization;
@@ -70,30 +70,31 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
         }
 
         /// <inheritdoc />
-        public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+        public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
         {
             _logger.LogInformation("Optimizing and vacuuming jellyfin.db...");
 
             try
             {
-                using var context = _provider.CreateContext();
-                if (context.Database.IsSqlite())
+                var context = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+                await using (context.ConfigureAwait(false))
                 {
-                    context.Database.ExecuteSqlRaw("PRAGMA optimize");
-                    context.Database.ExecuteSqlRaw("VACUUM");
-                    _logger.LogInformation("jellyfin.db optimized successfully!");
-                }
-                else
-                {
-                    _logger.LogInformation("This database doesn't support optimization");
+                    if (context.Database.IsSqlite())
+                    {
+                        await context.Database.ExecuteSqlRawAsync("PRAGMA optimize", cancellationToken).ConfigureAwait(false);
+                        await context.Database.ExecuteSqlRawAsync("VACUUM", cancellationToken).ConfigureAwait(false);
+                        _logger.LogInformation("jellyfin.db optimized successfully!");
+                    }
+                    else
+                    {
+                        _logger.LogInformation("This database doesn't support optimization");
+                    }
                 }
             }
             catch (Exception e)
             {
                 _logger.LogError(e, "Error while optimizing jellyfin.db");
             }
-
-            return Task.CompletedTask;
         }
     }
 }

+ 11 - 15
Emby.Server.Implementations/TV/TVSeriesManager.cs

@@ -192,7 +192,6 @@ namespace Emby.Server.Implementations.TV
                 AncestorWithPresentationUniqueKey = null,
                 SeriesPresentationUniqueKey = seriesKey,
                 IncludeItemTypes = new[] { BaseItemKind.Episode },
-                OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) },
                 IsPlayed = true,
                 Limit = 1,
                 ParentIndexNumberNotEquals = 0,
@@ -203,11 +202,10 @@ namespace Emby.Server.Implementations.TV
                 }
             };
 
-            if (rewatching)
-            {
-                // find last watched by date played, not by newest episode watched
-                lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
-            }
+            // If rewatching is enabled, sort first by date played and then by season and episode numbers
+            lastQuery.OrderBy = rewatching
+                ? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
+                : new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
 
             var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault();
 
@@ -226,18 +224,16 @@ namespace Emby.Server.Implementations.TV
                     DtoOptions = dtoOptions
                 };
 
-                Episode nextEpisode;
-                if (rewatching)
-                {
-                    nextQuery.Limit = 2;
-                    // get watched episode after most recently watched
-                    nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1);
-                }
-                else
+                // Locate the next up episode based on the last watched episode's season and episode number
+                var lastWatchedParentIndexNumber = lastWatchedEpisode?.ParentIndexNumber;
+                var lastWatchedIndexNumber = lastWatchedEpisode?.IndexNumberEnd ?? lastWatchedEpisode?.IndexNumber;
+                if (lastWatchedParentIndexNumber.HasValue && lastWatchedIndexNumber.HasValue)
                 {
-                    nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+                    nextQuery.MinParentAndIndexNumber = (lastWatchedParentIndexNumber.Value, lastWatchedIndexNumber.Value + 1);
                 }
 
+                var nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault();
+
                 if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons)
                 {
                     var consideredEpisodes = _libraryManager.GetItemList(new InternalItemsQuery(user)

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

@@ -178,7 +178,7 @@ namespace Jellyfin.Api.Controllers
 
             foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
             {
-                var order = int.Parse(key.AsSpan().Slice("homesection".Length));
+                var order = int.Parse(key.AsSpan().Slice("homesection".Length), NumberStyles.Any, CultureInfo.InvariantCulture);
                 if (!Enum.TryParse<HomeSectionType>(displayPreferences.CustomPrefs[key], true, out var type))
                 {
                     type = order < 8 ? defaults[order] : HomeSectionType.None;

+ 13 - 39
Jellyfin.Api/Controllers/ItemsController.cs

@@ -87,9 +87,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
         /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
         /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
-        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
-        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
-        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
         /// <param name="isMovie">Optional filter for live tv movies.</param>
         /// <param name="isSeries">Optional filter for live tv series.</param>
         /// <param name="isNews">Optional filter for live tv news.</param>
@@ -100,7 +100,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
         /// <param name="searchTerm">Optional. Filter based on a search term.</param>
-        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
@@ -282,39 +282,13 @@ namespace Jellyfin.Api.Controllers
                 includeItemTypes = new[] { BaseItemKind.Playlist };
             }
 
-            var enabledChannels = isApiKey
-                ? Array.Empty<Guid>()
-                : user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels);
-
-            // api keys are always enabled for all folders
-            bool isInEnabledFolder = isApiKey
-                                     || Array.IndexOf(user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders), item.Id) != -1
-                                     // Assume all folders inside an EnabledChannel are enabled
-                                     || Array.IndexOf(enabledChannels, item.Id) != -1
-                                     // Assume all items inside an EnabledChannel are enabled
-                                     || Array.IndexOf(enabledChannels, item.ChannelId) != -1;
-
-            if (!isInEnabledFolder)
-            {
-                var collectionFolders = _libraryManager.GetCollectionFolders(item);
-                foreach (var collectionFolder in collectionFolders)
-                {
-                    // api keys never enter this block, so user is never null
-                    if (user!.GetPreferenceValues<Guid>(PreferenceKind.EnabledFolders).Contains(collectionFolder.Id))
-                    {
-                        isInEnabledFolder = true;
-                    }
-                }
-            }
-
-            // api keys are always enabled for all folders, so user is never null
             if (item is not UserRootFolder
-                && !isInEnabledFolder
-                && !user!.HasPermission(PermissionKind.EnableAllFolders)
-                && !user.HasPermission(PermissionKind.EnableAllChannels)
-                && !string.Equals(collectionType, CollectionType.Folders, StringComparison.OrdinalIgnoreCase))
+                // api keys can always access all folders
+                && !isApiKey
+                // check the item is visible for the user
+                && !item.IsVisible(user))
             {
-                _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user.Username, item.Name);
+                _logger.LogWarning("{UserName} is not permitted to access Library {ItemName}", user!.Username, item.Name);
                 return Unauthorized($"{user.Username} is not permitted to access Library {item.Name}.");
             }
 
@@ -562,9 +536,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
         /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
         /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
-        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
-        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
-        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
         /// <param name="isMovie">Optional filter for live tv movies.</param>
         /// <param name="isSeries">Optional filter for live tv series.</param>
         /// <param name="isNews">Optional filter for live tv news.</param>
@@ -575,7 +549,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
         /// <param name="searchTerm">Optional. Filter based on a search term.</param>
-        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>

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

@@ -485,7 +485,7 @@ namespace Jellyfin.Api.Controllers
         /// <response code="200">Media folders returned.</response>
         /// <returns>List of user media folders.</returns>
         [HttpGet("Library/MediaFolders")]
-        [Authorize(Policy = Policies.DefaultAuthorization)]
+        [Authorize(Policy = Policies.RequiresElevation)]
         [ProducesResponseType(StatusCodes.Status200OK)]
         public ActionResult<QueryResult<BaseItemDto>> GetMediaFolders([FromQuery] bool? isHidden)
         {

+ 10 - 10
Jellyfin.Api/Controllers/MoviesController.cs

@@ -193,7 +193,7 @@ namespace Jellyfin.Api.Controllers
                     new InternalItemsQuery(user)
                     {
                         Person = name,
-                        // Account for duplicates by imdb id, since the database doesn't support this yet
+                        // Account for duplicates by IMDb id, since the database doesn't support this yet
                         Limit = itemLimit + 2,
                         PersonTypes = new[] { PersonType.Director },
                         IncludeItemTypes = itemTypes.ToArray(),
@@ -232,15 +232,15 @@ namespace Jellyfin.Api.Controllers
             foreach (var name in names)
             {
                 var items = _libraryManager.GetItemList(new InternalItemsQuery(user)
-                    {
-                        Person = name,
-                        // Account for duplicates by imdb id, since the database doesn't support this yet
-                        Limit = itemLimit + 2,
-                        IncludeItemTypes = itemTypes.ToArray(),
-                        IsMovie = true,
-                        EnableGroupByMetadataKey = true,
-                        DtoOptions = dtoOptions
-                    }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
+                {
+                    Person = name,
+                    // Account for duplicates by IMDb id, since the database doesn't support this yet
+                    Limit = itemLimit + 2,
+                    IncludeItemTypes = itemTypes.ToArray(),
+                    IsMovie = true,
+                    EnableGroupByMetadataKey = true,
+                    DtoOptions = dtoOptions
+                }).GroupBy(i => i.GetProviderId(MediaBrowser.Model.Entities.MetadataProvider.Imdb) ?? Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture))
                     .Select(x => x.First())
                     .Take(itemLimit)
                     .ToList();

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

@@ -55,9 +55,9 @@ namespace Jellyfin.Api.Controllers
         /// <param name="minDateLastSavedForUser">Optional. The minimum last saved date for the current user. Format = ISO.</param>
         /// <param name="maxPremiereDate">Optional. The maximum premiere date. Format = ISO.</param>
         /// <param name="hasOverview">Optional filter by items that have an overview or not.</param>
-        /// <param name="hasImdbId">Optional filter by items that have an imdb id or not.</param>
-        /// <param name="hasTmdbId">Optional filter by items that have a tmdb id or not.</param>
-        /// <param name="hasTvdbId">Optional filter by items that have a tvdb id or not.</param>
+        /// <param name="hasImdbId">Optional filter by items that have an IMDb id or not.</param>
+        /// <param name="hasTmdbId">Optional filter by items that have a TMDb id or not.</param>
+        /// <param name="hasTvdbId">Optional filter by items that have a TVDb id or not.</param>
         /// <param name="isMovie">Optional filter for live tv movies.</param>
         /// <param name="isSeries">Optional filter for live tv series.</param>
         /// <param name="isNews">Optional filter for live tv news.</param>
@@ -68,7 +68,7 @@ namespace Jellyfin.Api.Controllers
         /// <param name="limit">Optional. The maximum number of records to return.</param>
         /// <param name="recursive">When searching within folders, this determines whether or not the search will be recursive. true/false.</param>
         /// <param name="searchTerm">Optional. Filter based on a search term.</param>
-        /// <param name="sortOrder">Sort Order - Ascending,Descending.</param>
+        /// <param name="sortOrder">Sort Order - Ascending, Descending.</param>
         /// <param name="parentId">Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
         /// <param name="fields">Optional. Specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.</param>
         /// <param name="excludeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>

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

@@ -351,7 +351,7 @@ namespace Jellyfin.Api.Helpers
             try
             {
                 // Parses npt times in the format of '10:19:25.7'
-                return TimeSpan.Parse(value).Ticks;
+                return TimeSpan.Parse(value, CultureInfo.InvariantCulture).Ticks;
             }
             catch
             {

+ 1 - 1
Jellyfin.Api/Jellyfin.Api.csproj

@@ -17,7 +17,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.9" />
+    <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.11" />
     <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
     <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />

+ 39 - 34
Jellyfin.Server.Implementations/Activity/ActivityManager.cs

@@ -15,13 +15,13 @@ namespace Jellyfin.Server.Implementations.Activity
     /// </summary>
     public class ActivityManager : IActivityManager
     {
-        private readonly JellyfinDbProvider _provider;
+        private readonly IDbContextFactory<JellyfinDb> _provider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ActivityManager"/> class.
         /// </summary>
         /// <param name="provider">The Jellyfin database provider.</param>
-        public ActivityManager(JellyfinDbProvider provider)
+        public ActivityManager(IDbContextFactory<JellyfinDb> provider)
         {
             _provider = provider;
         }
@@ -32,10 +32,12 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public async Task CreateAsync(ActivityLog entry)
         {
-            await using var dbContext = _provider.CreateContext();
-
-            dbContext.ActivityLogs.Add(entry);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.ActivityLogs.Add(entry);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
             EntryCreated?.Invoke(this, new GenericEventArgs<ActivityLogEntry>(ConvertToOldModel(entry)));
         }
@@ -43,44 +45,47 @@ namespace Jellyfin.Server.Implementations.Activity
         /// <inheritdoc/>
         public async Task<QueryResult<ActivityLogEntry>> GetPagedResultAsync(ActivityLogQuery query)
         {
-            await using var dbContext = _provider.CreateContext();
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                IQueryable<ActivityLog> entries = dbContext.ActivityLogs
+                    .OrderByDescending(entry => entry.DateCreated);
 
-            IQueryable<ActivityLog> entries = dbContext.ActivityLogs
-                .AsQueryable()
-                .OrderByDescending(entry => entry.DateCreated);
+                if (query.MinDate.HasValue)
+                {
+                    entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
+                }
 
-            if (query.MinDate.HasValue)
-            {
-                entries = entries.Where(entry => entry.DateCreated >= query.MinDate);
-            }
+                if (query.HasUserId.HasValue)
+                {
+                    entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
+                }
 
-            if (query.HasUserId.HasValue)
-            {
-                entries = entries.Where(entry => (!entry.UserId.Equals(default)) == query.HasUserId.Value);
+                return new QueryResult<ActivityLogEntry>(
+                    query.Skip,
+                    await entries.CountAsync().ConfigureAwait(false),
+                    await entries
+                        .Skip(query.Skip ?? 0)
+                        .Take(query.Limit ?? 100)
+                        .AsAsyncEnumerable()
+                        .Select(ConvertToOldModel)
+                        .ToListAsync()
+                        .ConfigureAwait(false));
             }
-
-            return new QueryResult<ActivityLogEntry>(
-                query.Skip,
-                await entries.CountAsync().ConfigureAwait(false),
-                await entries
-                    .Skip(query.Skip ?? 0)
-                    .Take(query.Limit ?? 100)
-                    .AsAsyncEnumerable()
-                    .Select(ConvertToOldModel)
-                    .ToListAsync()
-                    .ConfigureAwait(false));
         }
 
         /// <inheritdoc />
         public async Task CleanAsync(DateTime startDate)
         {
-            await using var dbContext = _provider.CreateContext();
-            var entries = dbContext.ActivityLogs
-                .AsQueryable()
-                .Where(entry => entry.DateCreated <= startDate);
+            var dbContext = await _provider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var entries = dbContext.ActivityLogs
+                    .Where(entry => entry.DateCreated <= startDate);
 
-            dbContext.RemoveRange(entries);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.RemoveRange(entries);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         private static ActivityLogEntry ConvertToOldModel(ActivityLog entry)

+ 94 - 74
Jellyfin.Server.Implementations/Devices/DeviceManager.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
 using System.Linq;
 using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
@@ -22,7 +23,7 @@ namespace Jellyfin.Server.Implementations.Devices
     /// </summary>
     public class DeviceManager : IDeviceManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
         private readonly IUserManager _userManager;
         private readonly ConcurrentDictionary<string, ClientCapabilities> _capabilitiesMap = new();
 
@@ -31,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Devices
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
         /// <param name="userManager">The user manager.</param>
-        public DeviceManager(JellyfinDbProvider dbProvider, IUserManager userManager)
+        public DeviceManager(IDbContextFactory<JellyfinDb> dbProvider, IUserManager userManager)
         {
             _dbProvider = dbProvider;
             _userManager = userManager;
@@ -49,39 +50,50 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task UpdateDeviceOptions(string deviceId, string deviceName)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
-            if (deviceOptions == null)
+            DeviceOptions? deviceOptions;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                deviceOptions = new DeviceOptions(deviceId);
-                dbContext.DeviceOptions.Add(deviceOptions);
+                deviceOptions = await dbContext.DeviceOptions.AsQueryable().FirstOrDefaultAsync(dev => dev.DeviceId == deviceId).ConfigureAwait(false);
+                if (deviceOptions == null)
+                {
+                    deviceOptions = new DeviceOptions(deviceId);
+                    dbContext.DeviceOptions.Add(deviceOptions);
+                }
+
+                deviceOptions.CustomName = deviceName;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
             }
 
-            deviceOptions.CustomName = deviceName;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
-
             DeviceOptionsUpdated?.Invoke(this, new GenericEventArgs<Tuple<string, DeviceOptions>>(new Tuple<string, DeviceOptions>(deviceId, deviceOptions)));
         }
 
         /// <inheritdoc />
         public async Task<Device> CreateDevice(Device device)
         {
-            await using var dbContext = _dbProvider.CreateContext();
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Devices.Add(device);
 
-            dbContext.Devices.Add(device);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
             return device;
         }
 
         /// <inheritdoc />
         public async Task<DeviceOptions> GetDeviceOptions(string deviceId)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var deviceOptions = await dbContext.DeviceOptions
-                .AsQueryable()
-                .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
-                .ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            DeviceOptions? deviceOptions;
+            await using (dbContext.ConfigureAwait(false))
+            {
+                deviceOptions = await dbContext.DeviceOptions
+                    .AsNoTracking()
+                    .FirstOrDefaultAsync(d => d.DeviceId == deviceId)
+                    .ConfigureAwait(false);
+            }
 
             return deviceOptions ?? new DeviceOptions(deviceId);
         }
@@ -97,14 +109,17 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<DeviceInfo?> GetDevice(string id)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var device = await dbContext.Devices
-                .AsQueryable()
-                .Where(d => d.DeviceId == id)
-                .OrderByDescending(d => d.DateLastActivity)
-                .Include(d => d.User)
-                .FirstOrDefaultAsync()
-                .ConfigureAwait(false);
+            Device? device;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                device = await dbContext.Devices
+                    .Where(d => d.DeviceId == id)
+                    .OrderByDescending(d => d.DateLastActivity)
+                    .Include(d => d.User)
+                    .FirstOrDefaultAsync()
+                    .ConfigureAwait(false);
+            }
 
             var deviceInfo = device == null ? null : ToDeviceInfo(device);
 
@@ -114,41 +129,40 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<QueryResult<Device>> GetDevices(DeviceQuery query)
         {
-            await using var dbContext = _dbProvider.CreateContext();
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var devices = dbContext.Devices.AsQueryable();
 
-            var devices = dbContext.Devices.AsQueryable();
+                if (query.UserId.HasValue)
+                {
+                    devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
+                }
 
-            if (query.UserId.HasValue)
-            {
-                devices = devices.Where(device => device.UserId.Equals(query.UserId.Value));
-            }
+                if (query.DeviceId != null)
+                {
+                    devices = devices.Where(device => device.DeviceId == query.DeviceId);
+                }
 
-            if (query.DeviceId != null)
-            {
-                devices = devices.Where(device => device.DeviceId == query.DeviceId);
-            }
+                if (query.AccessToken != null)
+                {
+                    devices = devices.Where(device => device.AccessToken == query.AccessToken);
+                }
 
-            if (query.AccessToken != null)
-            {
-                devices = devices.Where(device => device.AccessToken == query.AccessToken);
-            }
+                var count = await devices.CountAsync().ConfigureAwait(false);
 
-            var count = await devices.CountAsync().ConfigureAwait(false);
+                if (query.Skip.HasValue)
+                {
+                    devices = devices.Skip(query.Skip.Value);
+                }
 
-            if (query.Skip.HasValue)
-            {
-                devices = devices.Skip(query.Skip.Value);
-            }
+                if (query.Limit.HasValue)
+                {
+                    devices = devices.Take(query.Limit.Value);
+                }
 
-            if (query.Limit.HasValue)
-            {
-                devices = devices.Take(query.Limit.Value);
+                return new QueryResult<Device>(query.Skip, count, await devices.ToListAsync().ConfigureAwait(false));
             }
-
-            return new QueryResult<Device>(
-                query.Skip,
-                count,
-                await devices.ToListAsync().ConfigureAwait(false));
         }
 
         /// <inheritdoc />
@@ -165,37 +179,43 @@ namespace Jellyfin.Server.Implementations.Devices
         /// <inheritdoc />
         public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var sessions = dbContext.Devices
-                .Include(d => d.User)
-                .AsQueryable()
-                .OrderByDescending(d => d.DateLastActivity)
-                .ThenBy(d => d.DeviceId)
-                .AsAsyncEnumerable();
-
-            if (supportsSync.HasValue)
+            IAsyncEnumerable<Device> sessions;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
-            }
+                sessions = dbContext.Devices
+                    .Include(d => d.User)
+                    .OrderByDescending(d => d.DateLastActivity)
+                    .ThenBy(d => d.DeviceId)
+                    .AsAsyncEnumerable();
 
-            if (userId.HasValue)
-            {
-                var user = _userManager.GetUserById(userId.Value);
+                if (supportsSync.HasValue)
+                {
+                    sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
+                }
 
-                sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
-            }
+                if (userId.HasValue)
+                {
+                    var user = _userManager.GetUserById(userId.Value);
+
+                    sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId));
+                }
 
-            var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
+                var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false);
 
-            return new QueryResult<DeviceInfo>(array);
+                return new QueryResult<DeviceInfo>(array);
+            }
         }
 
         /// <inheritdoc />
         public async Task DeleteDevice(Device device)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Devices.Remove(device);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Devices.Remove(device);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />

+ 43 - 0
Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs

@@ -0,0 +1,43 @@
+using System;
+using System.IO;
+using EFCoreSecondLevelCacheInterceptor;
+using MediaBrowser.Common.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace Jellyfin.Server.Implementations.Extensions;
+
+/// <summary>
+/// Extensions for the <see cref="IServiceCollection"/> interface.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+    /// <summary>
+    /// Adds the <see cref="IDbContextFactory{TContext}"/> interface to the service collection with second level caching enabled.
+    /// </summary>
+    /// <param name="serviceCollection">An instance of the <see cref="IServiceCollection"/> interface.</param>
+    /// <returns>The updated service collection.</returns>
+    public static IServiceCollection AddJellyfinDbContext(this IServiceCollection serviceCollection)
+    {
+        serviceCollection.AddEFSecondLevelCache(options =>
+            options.UseMemoryCacheProvider()
+                .CacheAllQueries(CacheExpirationMode.Sliding, TimeSpan.FromMinutes(10))
+                .DisableLogging(true)
+                .UseCacheKeyPrefix("EF_")
+                // Don't cache null values. Remove this optional setting if it's not necessary.
+                .SkipCachingResults(result =>
+                    result.Value == null || (result.Value is EFTableRows rows && rows.RowsCount == 0)));
+
+        serviceCollection.AddPooledDbContextFactory<JellyfinDb>((serviceProvider, opt) =>
+        {
+            var applicationPaths = serviceProvider.GetRequiredService<IApplicationPaths>();
+            var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
+            opt.UseSqlite($"Filename={Path.Combine(applicationPaths.DataPath, "jellyfin.db")}")
+                .AddInterceptors(serviceProvider.GetRequiredService<SecondLevelCacheInterceptor>())
+                .UseLoggerFactory(loggerFactory);
+        });
+
+        return serviceCollection;
+    }
+}

+ 5 - 4
Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj

@@ -26,14 +26,15 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="EFCoreSecondLevelCacheInterceptor" Version="3.7.5" />
     <PackageReference Include="System.Linq.Async" Version="6.0.1" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.9" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.9" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.11" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.11">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.9">
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.11">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 0 - 51
Jellyfin.Server.Implementations/JellyfinDbProvider.cs

@@ -1,51 +0,0 @@
-using System;
-using System.IO;
-using System.Linq;
-using MediaBrowser.Common.Configuration;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Implementations
-{
-    /// <summary>
-    /// Factory class for generating new <see cref="JellyfinDb"/> instances.
-    /// </summary>
-    public class JellyfinDbProvider
-    {
-        private readonly IServiceProvider _serviceProvider;
-        private readonly IApplicationPaths _appPaths;
-        private readonly ILogger<JellyfinDbProvider> _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
-        /// </summary>
-        /// <param name="serviceProvider">The application's service provider.</param>
-        /// <param name="appPaths">The application paths.</param>
-        /// <param name="logger">The logger.</param>
-        public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths, ILogger<JellyfinDbProvider> logger)
-        {
-            _serviceProvider = serviceProvider;
-            _appPaths = appPaths;
-            _logger = logger;
-
-            using var jellyfinDb = CreateContext();
-            if (jellyfinDb.Database.GetPendingMigrations().Any())
-            {
-                _logger.LogInformation("There are pending EFCore migrations in the database. Applying... (This may take a while, do not stop Jellyfin)");
-                jellyfinDb.Database.Migrate();
-                _logger.LogInformation("EFCore migrations applied successfully");
-            }
-        }
-
-        /// <summary>
-        /// Creates a new <see cref="JellyfinDb"/> context.
-        /// </summary>
-        /// <returns>The newly created context.</returns>
-        public JellyfinDb CreateContext()
-        {
-            var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
-            return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
-        }
-    }
-}

+ 657 - 0
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.Designer.cs

@@ -0,0 +1,657 @@
+#pragma warning disable CS1591
+
+// <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(JellyfinDb))]
+    [Migration("20221022080052_AddIndexActivityLogsDateCreated")]
+    partial class AddIndexActivityLogsDateCreated
+    {
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasDefaultSchema("jellyfin")
+                .HasAnnotation("ProductVersion", "6.0.9");
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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", "jellyfin");
+                });
+
+            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<string>("EasyPassword")
+                        .HasMaxLength(65535)
+                        .HasColumnType("TEXT");
+
+                    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", "jellyfin");
+                });
+
+            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
+        }
+    }
+}

+ 28 - 0
Jellyfin.Server.Implementations/Migrations/20221022080052_AddIndexActivityLogsDateCreated.cs

@@ -0,0 +1,28 @@
+#pragma warning disable CS1591, SA1601
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Jellyfin.Server.Implementations.Migrations
+{
+    public partial class AddIndexActivityLogsDateCreated : Migration
+    {
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateIndex(
+                name: "IX_ActivityLogs_DateCreated",
+                schema: "jellyfin",
+                table: "ActivityLogs",
+                column: "DateCreated");
+        }
+
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropIndex(
+                name: "IX_ActivityLogs_DateCreated",
+                schema: "jellyfin",
+                table: "ActivityLogs");
+        }
+    }
+}

+ 18 - 14
Jellyfin.Server.Implementations/Migrations/JellyfinDbModelSnapshot.cs

@@ -5,6 +5,8 @@ using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore.Infrastructure;
 using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
+#nullable disable
+
 namespace Jellyfin.Server.Implementations.Migrations
 {
     [DbContext(typeof(JellyfinDb))]
@@ -15,7 +17,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 #pragma warning disable 612, 618
             modelBuilder
                 .HasDefaultSchema("jellyfin")
-                .HasAnnotation("ProductVersion", "5.0.7");
+                .HasAnnotation("ProductVersion", "6.0.9");
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
                 {
@@ -39,7 +41,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("UserId");
 
-                    b.ToTable("AccessSchedules");
+                    b.ToTable("AccessSchedules", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
@@ -85,7 +87,9 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasKey("Id");
 
-                    b.ToTable("ActivityLogs");
+                    b.HasIndex("DateCreated");
+
+                    b.ToTable("ActivityLogs", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
@@ -117,7 +121,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId", "ItemId", "Client", "Key")
                         .IsUnique();
 
-                    b.ToTable("CustomItemDisplayPreferences");
+                    b.ToTable("CustomItemDisplayPreferences", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
@@ -174,7 +178,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId", "ItemId", "Client")
                         .IsUnique();
 
-                    b.ToTable("DisplayPreferences");
+                    b.ToTable("DisplayPreferences", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
@@ -196,7 +200,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("DisplayPreferencesId");
 
-                    b.ToTable("HomeSection");
+                    b.ToTable("HomeSection", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
@@ -221,7 +225,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("UserId")
                         .IsUnique();
 
-                    b.ToTable("ImageInfos");
+                    b.ToTable("ImageInfos", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
@@ -265,7 +269,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("UserId");
 
-                    b.ToTable("ItemDisplayPreferences");
+                    b.ToTable("ItemDisplayPreferences", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@@ -296,7 +300,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique()
                         .HasFilter("[UserId] IS NOT NULL");
 
-                    b.ToTable("Permissions");
+                    b.ToTable("Permissions", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@@ -329,7 +333,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                         .IsUnique()
                         .HasFilter("[UserId] IS NOT NULL");
 
-                    b.ToTable("Preferences");
+                    b.ToTable("Preferences", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
@@ -358,7 +362,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("AccessToken")
                         .IsUnique();
 
-                    b.ToTable("ApiKeys");
+                    b.ToTable("ApiKeys", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
@@ -416,7 +420,7 @@ namespace Jellyfin.Server.Implementations.Migrations
 
                     b.HasIndex("UserId", "DeviceId");
 
-                    b.ToTable("Devices");
+                    b.ToTable("Devices", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
@@ -437,7 +441,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("DeviceId")
                         .IsUnique();
 
-                    b.ToTable("DeviceOptions");
+                    b.ToTable("DeviceOptions", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
@@ -550,7 +554,7 @@ namespace Jellyfin.Server.Implementations.Migrations
                     b.HasIndex("Username")
                         .IsUnique();
 
-                    b.ToTable("Users");
+                    b.ToTable("Users", "jellyfin");
                 });
 
             modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>

+ 17 - 0
Jellyfin.Server.Implementations/ModelConfiguration/ActivityLogConfiguration.cs

@@ -0,0 +1,17 @@
+using Jellyfin.Data.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace Jellyfin.Server.Implementations.ModelConfiguration;
+
+/// <summary>
+/// FluentAPI configuration for the ActivityLog entity.
+/// </summary>
+public class ActivityLogConfiguration : IEntityTypeConfiguration<ActivityLog>
+{
+    /// <inheritdoc/>
+    public void Configure(EntityTypeBuilder<ActivityLog> builder)
+    {
+        builder.HasIndex(entity => entity.DateCreated);
+    }
+}

+ 38 - 32
Jellyfin.Server.Implementations/Security/AuthenticationManager.cs

@@ -10,13 +10,13 @@ namespace Jellyfin.Server.Implementations.Security
     /// <inheritdoc />
     public class AuthenticationManager : IAuthenticationManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AuthenticationManager"/> class.
         /// </summary>
         /// <param name="dbProvider">The database provider.</param>
-        public AuthenticationManager(JellyfinDbProvider dbProvider)
+        public AuthenticationManager(IDbContextFactory<JellyfinDb> dbProvider)
         {
             _dbProvider = dbProvider;
         }
@@ -24,50 +24,56 @@ namespace Jellyfin.Server.Implementations.Security
         /// <inheritdoc />
         public async Task CreateApiKey(string name)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            dbContext.ApiKeys.Add(new ApiKey(name));
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.ApiKeys.Add(new ApiKey(name));
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />
         public async Task<IReadOnlyList<AuthenticationInfo>> GetApiKeys()
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            return await dbContext.ApiKeys
-                .AsAsyncEnumerable()
-                .Select(key => new AuthenticationInfo
-                {
-                    AppName = key.Name,
-                    AccessToken = key.AccessToken,
-                    DateCreated = key.DateCreated,
-                    DeviceId = string.Empty,
-                    DeviceName = string.Empty,
-                    AppVersion = string.Empty
-                }).ToListAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                return await dbContext.ApiKeys
+                    .AsAsyncEnumerable()
+                    .Select(key => new AuthenticationInfo
+                    {
+                        AppName = key.Name,
+                        AccessToken = key.AccessToken,
+                        DateCreated = key.DateCreated,
+                        DeviceId = string.Empty,
+                        DeviceName = string.Empty,
+                        AppVersion = string.Empty
+                    }).ToListAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc />
         public async Task DeleteApiKey(string accessToken)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-
-            var key = await dbContext.ApiKeys
-                .AsQueryable()
-                .Where(apiKey => apiKey.AccessToken == accessToken)
-                .FirstOrDefaultAsync()
-                .ConfigureAwait(false);
-
-            if (key == null)
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                return;
-            }
+                var key = await dbContext.ApiKeys
+                    .AsQueryable()
+                    .Where(apiKey => apiKey.AccessToken == accessToken)
+                    .FirstOrDefaultAsync()
+                    .ConfigureAwait(false);
 
-            dbContext.Remove(key);
+                if (key == null)
+                {
+                    return;
+                }
+
+                dbContext.Remove(key);
 
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
     }
 }

+ 73 - 69
Jellyfin.Server.Implementations/Security/AuthorizationContext.cs

@@ -4,6 +4,7 @@ using System;
 using System.Collections.Generic;
 using System.Net;
 using System.Threading.Tasks;
+using EFCoreSecondLevelCacheInterceptor;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Net;
@@ -15,12 +16,12 @@ namespace Jellyfin.Server.Implementations.Security
 {
     public class AuthorizationContext : IAuthorizationContext
     {
-        private readonly JellyfinDbProvider _jellyfinDbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _jellyfinDbProvider;
         private readonly IUserManager _userManager;
         private readonly IServerApplicationHost _serverApplicationHost;
 
         public AuthorizationContext(
-            JellyfinDbProvider jellyfinDb,
+            IDbContextFactory<JellyfinDb> jellyfinDb,
             IUserManager userManager,
             IServerApplicationHost serverApplicationHost)
         {
@@ -121,96 +122,99 @@ namespace Jellyfin.Server.Implementations.Security
 #pragma warning restore CA1508
 
             authInfo.HasToken = true;
-            await using var dbContext = _jellyfinDbProvider.CreateContext();
-            var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
-
-            if (device != null)
+            var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                authInfo.IsAuthenticated = true;
-                var updateToken = false;
-
-                // TODO: Remove these checks for IsNullOrWhiteSpace
-                if (string.IsNullOrWhiteSpace(authInfo.Client))
-                {
-                    authInfo.Client = device.AppName;
-                }
+                var device = await dbContext.Devices.FirstOrDefaultAsync(d => d.AccessToken == token).ConfigureAwait(false);
 
-                if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                if (device != null)
                 {
-                    authInfo.DeviceId = device.DeviceId;
-                }
-
-                // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
-                var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
+                    authInfo.IsAuthenticated = true;
+                    var updateToken = false;
 
-                if (string.IsNullOrWhiteSpace(authInfo.Device))
-                {
-                    authInfo.Device = device.DeviceName;
-                }
-                else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
-                {
-                    if (allowTokenInfoUpdate)
+                    // TODO: Remove these checks for IsNullOrWhiteSpace
+                    if (string.IsNullOrWhiteSpace(authInfo.Client))
                     {
-                        updateToken = true;
-                        device.DeviceName = authInfo.Device;
+                        authInfo.Client = device.AppName;
                     }
-                }
 
-                if (string.IsNullOrWhiteSpace(authInfo.Version))
-                {
-                    authInfo.Version = device.AppVersion;
-                }
-                else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
-                {
-                    if (allowTokenInfoUpdate)
+                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
                     {
-                        updateToken = true;
-                        device.AppVersion = authInfo.Version;
+                        authInfo.DeviceId = device.DeviceId;
                     }
-                }
 
-                if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
-                {
-                    device.DateLastActivity = DateTime.UtcNow;
-                    updateToken = true;
-                }
+                    // Temporary. TODO - allow clients to specify that the token has been shared with a casting device
+                    var allowTokenInfoUpdate = !authInfo.Client.Contains("chromecast", StringComparison.OrdinalIgnoreCase);
 
-                authInfo.User = _userManager.GetUserById(device.UserId);
+                    if (string.IsNullOrWhiteSpace(authInfo.Device))
+                    {
+                        authInfo.Device = device.DeviceName;
+                    }
+                    else if (!string.Equals(authInfo.Device, device.DeviceName, StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (allowTokenInfoUpdate)
+                        {
+                            updateToken = true;
+                            device.DeviceName = authInfo.Device;
+                        }
+                    }
 
-                if (updateToken)
-                {
-                    dbContext.Devices.Update(device);
-                    await dbContext.SaveChangesAsync().ConfigureAwait(false);
-                }
-            }
-            else
-            {
-                var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
-                if (key != null)
-                {
-                    authInfo.IsAuthenticated = true;
-                    authInfo.Client = key.Name;
-                    authInfo.Token = key.AccessToken;
-                    if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                    if (string.IsNullOrWhiteSpace(authInfo.Version))
                     {
-                        authInfo.DeviceId = _serverApplicationHost.SystemId;
+                        authInfo.Version = device.AppVersion;
+                    }
+                    else if (!string.Equals(authInfo.Version, device.AppVersion, StringComparison.OrdinalIgnoreCase))
+                    {
+                        if (allowTokenInfoUpdate)
+                        {
+                            updateToken = true;
+                            device.AppVersion = authInfo.Version;
+                        }
                     }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Device))
+                    if ((DateTime.UtcNow - device.DateLastActivity).TotalMinutes > 3)
                     {
-                        authInfo.Device = _serverApplicationHost.Name;
+                        device.DateLastActivity = DateTime.UtcNow;
+                        updateToken = true;
                     }
 
-                    if (string.IsNullOrWhiteSpace(authInfo.Version))
+                    authInfo.User = _userManager.GetUserById(device.UserId);
+
+                    if (updateToken)
                     {
-                        authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+                        dbContext.Devices.Update(device);
+                        await dbContext.SaveChangesAsync().ConfigureAwait(false);
                     }
+                }
+                else
+                {
+                    var key = await dbContext.ApiKeys.FirstOrDefaultAsync(apiKey => apiKey.AccessToken == token).ConfigureAwait(false);
+                    if (key != null)
+                    {
+                        authInfo.IsAuthenticated = true;
+                        authInfo.Client = key.Name;
+                        authInfo.Token = key.AccessToken;
+                        if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
+                        {
+                            authInfo.DeviceId = _serverApplicationHost.SystemId;
+                        }
+
+                        if (string.IsNullOrWhiteSpace(authInfo.Device))
+                        {
+                            authInfo.Device = _serverApplicationHost.Name;
+                        }
+
+                        if (string.IsNullOrWhiteSpace(authInfo.Version))
+                        {
+                            authInfo.Version = _serverApplicationHost.ApplicationVersionString;
+                        }
 
-                    authInfo.IsApiKey = true;
+                        authInfo.IsApiKey = true;
+                    }
                 }
-            }
 
-            return authInfo;
+                return authInfo;
+            }
         }
 
         /// <summary>

+ 3 - 3
Jellyfin.Server.Implementations/Users/DisplayPreferencesManager.cs

@@ -20,10 +20,10 @@ namespace Jellyfin.Server.Implementations.Users
         /// <summary>
         /// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
         /// </summary>
-        /// <param name="dbContext">The database context.</param>
-        public DisplayPreferencesManager(JellyfinDb dbContext)
+        /// <param name="dbContextFactory">The database context factory.</param>
+        public DisplayPreferencesManager(IDbContextFactory<JellyfinDb> dbContextFactory)
         {
-            _dbContext = dbContext;
+            _dbContext = dbContextFactory.CreateDbContext();
         }
 
         /// <inheritdoc />

+ 162 - 131
Jellyfin.Server.Implementations/Users/UserManager.cs

@@ -33,7 +33,7 @@ namespace Jellyfin.Server.Implementations.Users
     /// </summary>
     public class UserManager : IUserManager
     {
-        private readonly JellyfinDbProvider _dbProvider;
+        private readonly IDbContextFactory<JellyfinDb> _dbProvider;
         private readonly IEventManager _eventManager;
         private readonly ICryptoProvider _cryptoProvider;
         private readonly INetworkManager _networkManager;
@@ -59,7 +59,7 @@ namespace Jellyfin.Server.Implementations.Users
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</param>
         public UserManager(
-            JellyfinDbProvider dbProvider,
+            IDbContextFactory<JellyfinDb> dbProvider,
             IEventManager eventManager,
             ICryptoProvider cryptoProvider,
             INetworkManager networkManager,
@@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Users
             _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
 
             _users = new ConcurrentDictionary<Guid, User>();
-            using var dbContext = _dbProvider.CreateContext();
+            using var dbContext = _dbProvider.CreateDbContext();
             foreach (var user in dbContext.Users
                 .Include(user => user.Permissions)
                 .Include(user => user.Preferences)
@@ -139,31 +139,35 @@ namespace Jellyfin.Server.Implementations.Users
                 throw new ArgumentException("The new and old names must be different.");
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-
-            if (await dbContext.Users
-                .AsQueryable()
-                .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
-                .ConfigureAwait(false))
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
             {
-                throw new ArgumentException(string.Format(
-                    CultureInfo.InvariantCulture,
-                    "A user with the name '{0}' already exists.",
-                    newName));
+                if (await dbContext.Users
+                        .AsQueryable()
+                        .AnyAsync(u => u.Username == newName && !u.Id.Equals(user.Id))
+                        .ConfigureAwait(false))
+                {
+                    throw new ArgumentException(string.Format(
+                        CultureInfo.InvariantCulture,
+                        "A user with the name '{0}' already exists.",
+                        newName));
+                }
+
+                user.Username = newName;
+                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
             }
 
-            user.Username = newName;
-            await UpdateUserAsync(user).ConfigureAwait(false);
             OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
         }
 
         /// <inheritdoc/>
         public async Task UpdateUserAsync(User user)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Users.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                await UpdateUserInternalAsync(dbContext, user).ConfigureAwait(false);
+            }
         }
 
         internal async Task<User> CreateUserInternalAsync(string name, JellyfinDb dbContext)
@@ -202,12 +206,15 @@ namespace Jellyfin.Server.Implementations.Users
                     name));
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-
-            var newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
+            User newUser;
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                newUser = await CreateUserInternalAsync(name, dbContext).ConfigureAwait(false);
 
-            dbContext.Users.Add(newUser);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.Users.Add(newUser);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
 
             await _eventManager.PublishAsync(new UserCreatedEventArgs(newUser)).ConfigureAwait(false);
 
@@ -241,9 +248,13 @@ namespace Jellyfin.Server.Implementations.Users
                     nameof(userId));
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Users.Remove(user);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Users.Remove(user);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
+
             _users.Remove(userId);
 
             await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
@@ -288,7 +299,7 @@ namespace Jellyfin.Server.Implementations.Users
             user.EasyPassword = newPasswordSha1;
             await UpdateUserAsync(user).ConfigureAwait(false);
 
-            _eventManager.Publish(new UserPasswordChangedEventArgs(user));
+            await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
         }
 
         /// <inheritdoc/>
@@ -541,14 +552,17 @@ namespace Jellyfin.Server.Implementations.Users
 
             _logger.LogWarning("No users, creating one with username {UserName}", defaultName);
 
-            await using var dbContext = _dbProvider.CreateContext();
-            var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
-            newUser.SetPermission(PermissionKind.IsAdministrator, true);
-            newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
-            newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
+                newUser.SetPermission(PermissionKind.IsAdministrator, true);
+                newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
+                newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
 
-            dbContext.Users.Add(newUser);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                dbContext.Users.Add(newUser);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
@@ -584,105 +598,111 @@ namespace Jellyfin.Server.Implementations.Users
         /// <inheritdoc/>
         public async Task UpdateConfigurationAsync(Guid userId, UserConfiguration config)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users
-                           .Include(u => u.Permissions)
-                           .Include(u => u.Preferences)
-                           .Include(u => u.AccessSchedules)
-                           .Include(u => u.ProfileImage)
-                           .FirstOrDefault(u => u.Id.Equals(userId))
-                       ?? throw new ArgumentException("No user exists with given Id!");
-
-            user.SubtitleMode = config.SubtitleMode;
-            user.HidePlayedInLatest = config.HidePlayedInLatest;
-            user.EnableLocalPassword = config.EnableLocalPassword;
-            user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
-            user.DisplayCollectionsView = config.DisplayCollectionsView;
-            user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
-            user.AudioLanguagePreference = config.AudioLanguagePreference;
-            user.RememberAudioSelections = config.RememberAudioSelections;
-            user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
-            user.RememberSubtitleSelections = config.RememberSubtitleSelections;
-            user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
-
-            user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
-            user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
-            user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
-            user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
-
-            dbContext.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var user = dbContext.Users
+                               .Include(u => u.Permissions)
+                               .Include(u => u.Preferences)
+                               .Include(u => u.AccessSchedules)
+                               .Include(u => u.ProfileImage)
+                               .FirstOrDefault(u => u.Id.Equals(userId))
+                           ?? throw new ArgumentException("No user exists with given Id!");
+
+                user.SubtitleMode = config.SubtitleMode;
+                user.HidePlayedInLatest = config.HidePlayedInLatest;
+                user.EnableLocalPassword = config.EnableLocalPassword;
+                user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
+                user.DisplayCollectionsView = config.DisplayCollectionsView;
+                user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
+                user.AudioLanguagePreference = config.AudioLanguagePreference;
+                user.RememberAudioSelections = config.RememberAudioSelections;
+                user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
+                user.RememberSubtitleSelections = config.RememberSubtitleSelections;
+                user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
+
+                user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
+                user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
+                user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
+                user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
+
+                dbContext.Update(user);
+                _users[user.Id] = user;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
         public async Task UpdatePolicyAsync(Guid userId, UserPolicy policy)
         {
-            await using var dbContext = _dbProvider.CreateContext();
-            var user = dbContext.Users
-                           .Include(u => u.Permissions)
-                           .Include(u => u.Preferences)
-                           .Include(u => u.AccessSchedules)
-                           .Include(u => u.ProfileImage)
-                           .FirstOrDefault(u => u.Id.Equals(userId))
-                       ?? throw new ArgumentException("No user exists with given Id!");
-
-            // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
-            int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
-            {
-                -1 => null,
-                0 => 3,
-                _ => policy.LoginAttemptsBeforeLockout
-            };
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                var user = dbContext.Users
+                               .Include(u => u.Permissions)
+                               .Include(u => u.Preferences)
+                               .Include(u => u.AccessSchedules)
+                               .Include(u => u.ProfileImage)
+                               .FirstOrDefault(u => u.Id.Equals(userId))
+                           ?? throw new ArgumentException("No user exists with given Id!");
+
+                // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
+                int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
+                {
+                    -1 => null,
+                    0 => 3,
+                    _ => policy.LoginAttemptsBeforeLockout
+                };
 
-            user.MaxParentalAgeRating = policy.MaxParentalRating;
-            user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
-            user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
-            user.AuthenticationProviderId = policy.AuthenticationProviderId;
-            user.PasswordResetProviderId = policy.PasswordResetProviderId;
-            user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
-            user.LoginAttemptsBeforeLockout = maxLoginAttempts;
-            user.MaxActiveSessions = policy.MaxActiveSessions;
-            user.SyncPlayAccess = policy.SyncPlayAccess;
-            user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
-            user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
-            user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
-            user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
-            user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
-            user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
-            user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
-            user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
-            user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
-            user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
-            user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
-            user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
-            user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
-            user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
-            user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
-            user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
-            user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
-            user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
-            user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
-            user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
-            user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
-
-            user.AccessSchedules.Clear();
-            foreach (var policyAccessSchedule in policy.AccessSchedules)
-            {
-                user.AccessSchedules.Add(policyAccessSchedule);
-            }
-
-            // TODO: fix this at some point
-            user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
-            user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
-            user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
-            user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
-            user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
-            user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
-
-            dbContext.Update(user);
-            _users[user.Id] = user;
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+                user.MaxParentalAgeRating = policy.MaxParentalRating;
+                user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
+                user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
+                user.AuthenticationProviderId = policy.AuthenticationProviderId;
+                user.PasswordResetProviderId = policy.PasswordResetProviderId;
+                user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
+                user.LoginAttemptsBeforeLockout = maxLoginAttempts;
+                user.MaxActiveSessions = policy.MaxActiveSessions;
+                user.SyncPlayAccess = policy.SyncPlayAccess;
+                user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
+                user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
+                user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
+                user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
+                user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
+                user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
+                user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
+                user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
+                user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
+                user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
+                user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
+                user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
+                user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
+                user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
+                user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
+                user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
+                user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
+                user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
+                user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
+                user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
+                user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
+
+                user.AccessSchedules.Clear();
+                foreach (var policyAccessSchedule in policy.AccessSchedules)
+                {
+                    user.AccessSchedules.Add(policyAccessSchedule);
+                }
+
+                // TODO: fix this at some point
+                user.SetPreference(PreferenceKind.BlockUnratedItems, policy.BlockUnratedItems ?? Array.Empty<UnratedItem>());
+                user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
+                user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
+                user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
+                user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
+                user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
+
+                dbContext.Update(user);
+                _users[user.Id] = user;
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
         }
 
         /// <inheritdoc/>
@@ -693,9 +713,13 @@ namespace Jellyfin.Server.Implementations.Users
                 return;
             }
 
-            await using var dbContext = _dbProvider.CreateContext();
-            dbContext.Remove(user.ProfileImage);
-            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
+            await using (dbContext.ConfigureAwait(false))
+            {
+                dbContext.Remove(user.ProfileImage);
+                await dbContext.SaveChangesAsync().ConfigureAwait(false);
+            }
+
             user.ProfileImage = null;
             _users[user.Id] = user;
         }
@@ -859,5 +883,12 @@ namespace Jellyfin.Server.Implementations.Users
 
             await UpdateUserAsync(user).ConfigureAwait(false);
         }
+
+        private async Task UpdateUserInternalAsync(JellyfinDb dbContext, User user)
+        {
+            dbContext.Users.Update(user);
+            _users[user.Id] = user;
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
+        }
     }
 }

+ 1 - 8
Jellyfin.Server/CoreAppHost.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Collections.Generic;
-using System.IO;
 using System.Reflection;
 using Emby.Drawing;
 using Emby.Server.Implementations;
@@ -71,19 +70,13 @@ namespace Jellyfin.Server
                 Logger.LogWarning("Skia not available. Will fallback to {ImageEncoder}.", nameof(NullImageEncoder));
             }
 
-            serviceCollection.AddDbContextPool<JellyfinDb>(
-                 options => options
-                    .UseLoggerFactory(LoggerFactory)
-                    .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"));
-
             serviceCollection.AddEventServices();
             serviceCollection.AddSingleton<IBaseItemManager, BaseItemManager>();
             serviceCollection.AddSingleton<IEventManager, EventManager>();
-            serviceCollection.AddSingleton<JellyfinDbProvider>();
 
             serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
             serviceCollection.AddSingleton<IUserManager, UserManager>();
-            serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
+            serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
             serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
 
             // TODO search the assemblies instead of adding them manually?

+ 9 - 5
Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs

@@ -434,11 +434,15 @@ namespace Jellyfin.Server.Extensions
             options.MapType<TranscodeReason>(() =>
                 new OpenApiSchema
                 {
-                    Type = "string",
-                    Enum = Enum.GetNames<TranscodeReason>()
-                        .Select(e => new OpenApiString(e))
-                        .Cast<IOpenApiAny>()
-                        .ToArray()
+                    Type = "array",
+                    Items = new OpenApiSchema
+                    {
+                        Reference = new OpenApiReference
+                        {
+                            Id = nameof(TranscodeReason),
+                            Type = ReferenceType.Schema,
+                        }
+                    }
                 });
 
             // Swashbuckle doesn't use JsonOptions to describe responses, so we need to manually describe it.

+ 11 - 0
Jellyfin.Server/Filters/AdditionalModelFilter.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using Jellyfin.Extensions;
 using Jellyfin.Server.Migrations;
 using MediaBrowser.Common.Plugins;
@@ -8,6 +9,7 @@ using MediaBrowser.Model.ApiClient;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.SyncPlay;
+using Microsoft.OpenApi.Any;
 using Microsoft.OpenApi.Models;
 using Swashbuckle.AspNetCore.SwaggerGen;
 
@@ -56,6 +58,15 @@ namespace Jellyfin.Server.Filters
 
                 context.SchemaGenerator.GenerateSchema(configuration.ConfigurationType, context.SchemaRepository);
             }
+
+            context.SchemaRepository.AddDefinition(nameof(TranscodeReason), new OpenApiSchema
+            {
+                Type = "string",
+                Enum = Enum.GetNames<TranscodeReason>()
+                    .Select(e => new OpenApiString(e))
+                    .Cast<IOpenApiAny>()
+                    .ToArray()
+            });
         }
     }
 }

+ 4 - 4
Jellyfin.Server/Jellyfin.Server.csproj

@@ -37,10 +37,10 @@
     <PackageReference Include="CommandLineParser" Version="2.9.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.9" />
-    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.9" />
-    <PackageReference Include="prometheus-net" Version="6.0.0" />
-    <PackageReference Include="prometheus-net.AspNetCore" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.11" />
+    <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.11" />
+    <PackageReference Include="prometheus-net" Version="7.0.0" />
+    <PackageReference Include="prometheus-net.AspNetCore" Version="7.0.0" />
     <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
     <PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
     <PackageReference Include="Serilog.Settings.Configuration" Version="3.4.0" />

+ 3 - 2
Jellyfin.Server/Middleware/BaseUrlRedirectionMiddleware.cs

@@ -65,8 +65,9 @@ namespace Jellyfin.Server.Middleware
                 // Always redirect back to the default path if the base prefix is invalid or missing
                 _logger.LogDebug("Normalizing an URL at {LocalPath}", localPath);
 
-                var uri = new Uri(localPath);
-                var redirectUri = new Uri(baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]);
+                var port = httpContext.Request.Host.Port ?? -1;
+                var uri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, localPath).Uri;
+                var redirectUri = new UriBuilder(httpContext.Request.Scheme, httpContext.Request.Host.Host, port, baseUrlPrefix + "/" + _configuration[DefaultRedirectKey]).Uri;
                 var target = uri.MakeRelativeUri(redirectUri).ToString();
                 _logger.LogDebug("Redirecting to {Target}", target);
 

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio