Browse Source

Merge branch 'master' into master

Joshua M. Boniface 2 days ago
parent
commit
4b6fb6c4bb
100 changed files with 2496 additions and 1377 deletions
  1. 1 1
      .config/dotnet-tools.json
  2. 7 3
      .devcontainer/devcontainer.json
  3. 7 2
      .github/ISSUE_TEMPLATE/issue report.yml
  4. 4 4
      .github/workflows/ci-codeql-analysis.yml
  5. 7 7
      .github/workflows/ci-compat.yml
  6. 12 12
      .github/workflows/ci-openapi.yml
  7. 2 2
      .github/workflows/ci-tests.yml
  8. 2 2
      .github/workflows/commands.yml
  9. 2 2
      .github/workflows/issue-template-check.yml
  10. 1 1
      .github/workflows/pull-request-conflict.yml
  11. 3 0
      .vscode/settings.json
  12. 8 0
      CONTRIBUTORS.md
  13. 49 43
      Directory.Packages.props
  14. 4 1
      Emby.Naming/Common/NamingOptions.cs
  15. 38 52
      Emby.Naming/TV/SeasonPathParser.cs
  16. 5 2
      Emby.Naming/Video/ExtraRuleResolver.cs
  17. 3 2
      Emby.Naming/Video/VideoListResolver.cs
  18. 9 6
      Emby.Naming/Video/VideoResolver.cs
  19. 2 2
      Emby.Photos/PhotoProvider.cs
  20. 59 45
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  21. 1 0
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  22. 32 27
      Emby.Server.Implementations/ApplicationHost.cs
  23. 313 0
      Emby.Server.Implementations/Chapters/ChapterManager.cs
  24. 7 7
      Emby.Server.Implementations/Collections/CollectionManager.cs
  25. 81 51
      Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs
  26. 20 19
      Emby.Server.Implementations/Dto/DtoService.cs
  27. 7 1
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  28. 1 1
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  29. 2 1
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  30. 1 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  31. 1 1
      Emby.Server.Implementations/IO/FileRefresher.cs
  32. 1 1
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  33. 24 9
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  34. 9 5
      Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs
  35. 1 0
      Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
  36. 1 0
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  37. 1 0
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  38. 1 0
      Emby.Server.Implementations/Images/MusicGenreImageProvider.cs
  39. 2 2
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  40. 94 0
      Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs
  41. 71 0
      Emby.Server.Implementations/Library/ExternalDataManager.cs
  42. 0 1
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  43. 44 0
      Emby.Server.Implementations/Library/KeyframeManager.cs
  44. 239 165
      Emby.Server.Implementations/Library/LibraryManager.cs
  45. 26 10
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  46. 61 27
      Emby.Server.Implementations/Library/MediaStreamSelector.cs
  47. 2 1
      Emby.Server.Implementations/Library/MusicManager.cs
  48. 101 0
      Emby.Server.Implementations/Library/PathManager.cs
  49. 16 6
      Emby.Server.Implementations/Library/ResolverHelper.cs
  50. 2 2
      Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
  51. 9 4
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  52. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  53. 4 3
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  54. 2 1
      Emby.Server.Implementations/Library/SearchEngine.cs
  55. 7 7
      Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs
  56. 8 10
      Emby.Server.Implementations/Library/UserDataManager.cs
  57. 18 1
      Emby.Server.Implementations/Library/UserViewManager.cs
  58. 34 35
      Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs
  59. 78 79
      Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs
  60. 107 110
      Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
  61. 34 35
      Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs
  62. 79 57
      Emby.Server.Implementations/Library/Validators/GenresValidator.cs
  63. 34 35
      Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs
  64. 58 59
      Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs
  65. 88 93
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  66. 34 35
      Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs
  67. 75 76
      Emby.Server.Implementations/Library/Validators/StudiosValidator.cs
  68. 7 1
      Emby.Server.Implementations/Localization/Core/af.json
  69. 5 3
      Emby.Server.Implementations/Localization/Core/ar.json
  70. 5 3
      Emby.Server.Implementations/Localization/Core/be.json
  71. 3 1
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  72. 47 41
      Emby.Server.Implementations/Localization/Core/bn.json
  73. 43 41
      Emby.Server.Implementations/Localization/Core/ca.json
  74. 3 1
      Emby.Server.Implementations/Localization/Core/cs.json
  75. 3 1
      Emby.Server.Implementations/Localization/Core/da.json
  76. 24 22
      Emby.Server.Implementations/Localization/Core/de.json
  77. 10 10
      Emby.Server.Implementations/Localization/Core/el.json
  78. 3 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  79. 3 1
      Emby.Server.Implementations/Localization/Core/en-US.json
  80. 5 1
      Emby.Server.Implementations/Localization/Core/eo.json
  81. 3 1
      Emby.Server.Implementations/Localization/Core/es.json
  82. 3 1
      Emby.Server.Implementations/Localization/Core/eu.json
  83. 3 1
      Emby.Server.Implementations/Localization/Core/fi.json
  84. 3 1
      Emby.Server.Implementations/Localization/Core/fr-CA.json
  85. 3 1
      Emby.Server.Implementations/Localization/Core/fr.json
  86. 3 1
      Emby.Server.Implementations/Localization/Core/ga.json
  87. 14 1
      Emby.Server.Implementations/Localization/Core/gl.json
  88. 13 11
      Emby.Server.Implementations/Localization/Core/he.json
  89. 1 0
      Emby.Server.Implementations/Localization/Core/he_IL.json
  90. 5 3
      Emby.Server.Implementations/Localization/Core/hu.json
  91. 9 1
      Emby.Server.Implementations/Localization/Core/id.json
  92. 4 1
      Emby.Server.Implementations/Localization/Core/is.json
  93. 5 3
      Emby.Server.Implementations/Localization/Core/it.json
  94. 3 1
      Emby.Server.Implementations/Localization/Core/ja.json
  95. 10 2
      Emby.Server.Implementations/Localization/Core/kn.json
  96. 47 45
      Emby.Server.Implementations/Localization/Core/lt-LT.json
  97. 3 1
      Emby.Server.Implementations/Localization/Core/lv.json
  98. 6 1
      Emby.Server.Implementations/Localization/Core/lzh.json
  99. 131 4
      Emby.Server.Implementations/Localization/Core/mn.json
  100. 9 2
      Emby.Server.Implementations/Localization/Core/mr.json

+ 1 - 1
.config/dotnet-tools.json

@@ -3,7 +3,7 @@
   "isRoot": true,
   "isRoot": true,
   "tools": {
   "tools": {
     "dotnet-ef": {
     "dotnet-ef": {
-      "version": "9.0.2",
+      "version": "9.0.7",
       "commands": [
       "commands": [
         "dotnet-ef"
         "dotnet-ef"
       ]
       ]

+ 7 - 3
.devcontainer/devcontainer.json

@@ -1,6 +1,8 @@
 {
 {
     "name": "Development Jellyfin Server",
     "name": "Development Jellyfin Server",
-    "image":"mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
+    "image": "mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm",
+    "service": "app",
+    "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
     // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
     // restores nuget packages, installs the dotnet workloads and installs the dev https certificate
     "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
     "postStartCommand": "sudo dotnet restore; sudo dotnet workload update; sudo dotnet dev-certs https --trust; sudo bash \"./.devcontainer/install-ffmpeg.sh\"",
     // reads the extensions list and installs them
     // reads the extensions list and installs them
@@ -11,9 +13,11 @@
             "dotnetRuntimeVersions": "9.0",
             "dotnetRuntimeVersions": "9.0",
             "aspNetCoreRuntimeVersions": "9.0"
             "aspNetCoreRuntimeVersions": "9.0"
         },
         },
-        "ghcr.io/devcontainers-contrib/features/apt-packages:1": {
+        "ghcr.io/devcontainers-extra/features/apt-packages:1": {
             "preserve_apt_list": false,
             "preserve_apt_list": false,
-            "packages": ["libfontconfig1"]
+            "packages": [
+                "libfontconfig1"
+            ]
         },
         },
         "ghcr.io/devcontainers/features/docker-in-docker:2": {
         "ghcr.io/devcontainers/features/docker-in-docker:2": {
             "dockerDashComposeVersion": "v2"
             "dockerDashComposeVersion": "v2"

+ 7 - 2
.github/ISSUE_TEMPLATE/issue report.yml

@@ -1,6 +1,7 @@
 name: Issue Report
 name: Issue Report
 description: File an issue report
 description: File an issue report
 labels: [bug, triage]
 labels: [bug, triage]
+type: Bug
 body:
 body:
   - type: markdown
   - type: markdown
     id: introduction
     id: introduction
@@ -140,7 +141,9 @@ body:
         - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
         - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
         - **Base URL**: [e.g. none, yes: /example]
         - **Base URL**: [e.g. none, yes: /example]
         - **Networking**: [e.g. Host, Bridge/NAT]
         - **Networking**: [e.g. Host, Bridge/NAT]
-        - **Storage**: [e.g. local, NFS, cloud]
+        - **Jellyfin Data Storage**: [e.g. local SATA SSD, local HDD]
+        - **Media Storage**: [e.g. Local HDD, SMB Share]
+        - **External Integrations**: [e.g. Jellystat, Jellyseerr]
       value: |
       value: |
         - OS:
         - OS:
         - Linux Kernel:
         - Linux Kernel:
@@ -155,7 +158,9 @@ body:
         - Reverse Proxy:
         - Reverse Proxy:
         - Base URL:
         - Base URL:
         - Networking:
         - Networking:
-        - Storage:
+        - Jellyfin Data Storage:
+        - Media Storage:
+        - External Integrations:
       render: markdown
       render: markdown
     validations:
     validations:
       required: true
       required: true

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

@@ -22,16 +22,16 @@ jobs:
     - name: Checkout repository
     - name: Checkout repository
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
     - name: Setup .NET
     - name: Setup .NET
-      uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+      uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
       with:
       with:
         dotnet-version: '9.0.x'
         dotnet-version: '9.0.x'
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/init@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         queries: +security-extended
         queries: +security-extended
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/autobuild@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
+      uses: github/codeql-action/analyze@51f77329afa6477de8c49fc9c7046c15b9a4e79d # v3.29.5

+ 7 - 7
.github/workflows/ci-compat.yml

@@ -17,7 +17,7 @@ jobs:
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
 
 
       - name: Setup .NET
       - name: Setup .NET
-        uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+        uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
         with:
         with:
           dotnet-version: '9.0.x'
           dotnet-version: '9.0.x'
 
 
@@ -26,7 +26,7 @@ jobs:
           dotnet build Jellyfin.Server -o ./out
           dotnet build Jellyfin.Server -o ./out
 
 
       - name: Upload Head
       - name: Upload Head
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
         with:
           name: abi-head
           name: abi-head
           retention-days: 14
           retention-days: 14
@@ -47,7 +47,7 @@ jobs:
           fetch-depth: 0
           fetch-depth: 0
 
 
       - name: Setup .NET
       - name: Setup .NET
-        uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+        uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
         with:
         with:
           dotnet-version: '9.0.x'
           dotnet-version: '9.0.x'
 
 
@@ -65,7 +65,7 @@ jobs:
           dotnet build Jellyfin.Server -o ./out
           dotnet build Jellyfin.Server -o ./out
 
 
       - name: Upload Head
       - name: Upload Head
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
         with:
           name: abi-base
           name: abi-base
           retention-days: 14
           retention-days: 14
@@ -85,13 +85,13 @@ jobs:
 
 
     steps:
     steps:
       - name: Download abi-head
       - name: Download abi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: abi-head
           name: abi-head
           path: abi-head
           path: abi-head
 
 
       - name: Download abi-base
       - name: Download abi-base
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: abi-base
           name: abi-base
           path: abi-base
           path: abi-base
@@ -105,7 +105,7 @@ jobs:
         run: |
         run: |
           {
           {
             echo 'body<<EOF'
             echo 'body<<EOF'
-            for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll; do
+            for file in Jellyfin.Data.dll MediaBrowser.Common.dll MediaBrowser.Controller.dll MediaBrowser.Model.dll Emby.Naming.dll Jellyfin.Extensions.dll Jellyfin.MediaEncoding.Keyframes.dll Jellyfin.Database.Implementations.dll; do
               COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
               COMPAT_OUTPUT="$( { apicompat --left ./abi-base/${file} --right ./abi-head/${file}; } 2>&1 )"
               if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
               if [ "APICompat ran successfully without finding any breaking changes." != "${COMPAT_OUTPUT}" ]; then
                 printf "\n${file}\n${COMPAT_OUTPUT}\n"
                 printf "\n${file}\n${COMPAT_OUTPUT}\n"

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

@@ -21,13 +21,13 @@ jobs:
           ref: ${{ github.event.pull_request.head.sha }}
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Setup .NET
       - name: Setup .NET
-        uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+        uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
         with:
         with:
           dotnet-version: '9.0.x'
           dotnet-version: '9.0.x'
       - name: Generate openapi.json
       - 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"
         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
       - name: Upload openapi.json
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
         with:
           name: openapi-head
           name: openapi-head
           retention-days: 14
           retention-days: 14
@@ -55,13 +55,13 @@ jobs:
           ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
           ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
           git checkout --progress --force $ANCESTOR_REF
           git checkout --progress --force $ANCESTOR_REF
       - name: Setup .NET
       - name: Setup .NET
-        uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+        uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
         with:
         with:
           dotnet-version: '9.0.x'
           dotnet-version: '9.0.x'
       - name: Generate openapi.json
       - 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"
         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
       - name: Upload openapi.json
-        uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
+        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
         with:
         with:
           name: openapi-base
           name: openapi-base
           retention-days: 14
           retention-days: 14
@@ -80,12 +80,12 @@ jobs:
       - openapi-base
       - openapi-base
     steps:
     steps:
       - name: Download openapi-head
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: openapi-head
           name: openapi-head
           path: openapi-head
           path: openapi-head
       - name: Download openapi-base
       - name: Download openapi-base
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: openapi-base
           name: openapi-base
           path: openapi-base
           path: openapi-base
@@ -158,12 +158,12 @@ jobs:
         run: |-
         run: |-
           echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
           echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV
       - name: Download openapi-head
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: openapi-head
           name: openapi-head
           path: openapi-head
           path: openapi-head
       - name: Upload openapi.json (unstable) to repository server
       - name: Upload openapi.json (unstable) to repository server
-        uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
+        uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
         with:
         with:
           host: "${{ secrets.REPO_HOST }}"
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"
           username: "${{ secrets.REPO_USER }}"
@@ -172,7 +172,7 @@ jobs:
           strip_components: 1
           strip_components: 1
           target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
           target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
       - name: Move openapi.json (unstable) into place
       - name: Move openapi.json (unstable) into place
-        uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
+        uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
         with:
         with:
           host: "${{ secrets.REPO_HOST }}"
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"
           username: "${{ secrets.REPO_USER }}"
@@ -220,12 +220,12 @@ jobs:
         run: |-
         run: |-
           echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
           echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
       - name: Download openapi-head
       - name: Download openapi-head
-        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
         with:
         with:
           name: openapi-head
           name: openapi-head
           path: openapi-head
           path: openapi-head
       - name: Upload openapi.json (stable) to repository server
       - name: Upload openapi.json (stable) to repository server
-        uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7
+        uses: appleboy/scp-action@ff85246acaad7bdce478db94a363cd2bf7c90345 # v1.0.0
         with:
         with:
           host: "${{ secrets.REPO_HOST }}"
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"
           username: "${{ secrets.REPO_USER }}"
@@ -234,7 +234,7 @@ jobs:
           strip_components: 1
           strip_components: 1
           target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
           target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}"
       - name: Move openapi.json (stable) into place
       - name: Move openapi.json (stable) into place
-        uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1
+        uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2
         with:
         with:
           host: "${{ secrets.REPO_HOST }}"
           host: "${{ secrets.REPO_HOST }}"
           username: "${{ secrets.REPO_USER }}"
           username: "${{ secrets.REPO_USER }}"

+ 2 - 2
.github/workflows/ci-tests.yml

@@ -22,7 +22,7 @@ jobs:
     steps:
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
 
 
-      - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0
+      - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
         with:
         with:
           dotnet-version: ${{ env.SDK_VERSION }}
           dotnet-version: ${{ env.SDK_VERSION }}
 
 
@@ -35,7 +35,7 @@ jobs:
           --verbosity minimal
           --verbosity minimal
 
 
       - name: Merge code coverage results
       - name: Merge code coverage results
-        uses: danielpalme/ReportGenerator-GitHub-Action@f1927db1dbfc029b056583ee488832e939447fe6 # v5.4.4
+        uses: danielpalme/ReportGenerator-GitHub-Action@c1dd332d00304c5aa5d506aab698a5224a8fa24e # 5.4.11
         with:
         with:
           reports: "**/coverage.cobertura.xml"
           reports: "**/coverage.cobertura.xml"
           targetdir: "merged/"
           targetdir: "merged/"

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

@@ -44,9 +44,9 @@ jobs:
         with:
         with:
           repository: jellyfin/jellyfin-triage-script
           repository: jellyfin/jellyfin-triage-script
       - name: install python
       - name: install python
-        uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
         with:
         with:
-          python-version: '3.12'
+          python-version: '3.13'
           cache: 'pip'
           cache: 'pip'
       - name: install python packages
       - name: install python packages
         run: pip install -r rename/requirements.txt
         run: pip install -r rename/requirements.txt

+ 2 - 2
.github/workflows/issue-template-check.yml

@@ -14,9 +14,9 @@ jobs:
         with:
         with:
           repository: jellyfin/jellyfin-triage-script
           repository: jellyfin/jellyfin-triage-script
       - name: install python
       - name: install python
-        uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
+        uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
         with:
         with:
-          python-version: '3.12'
+          python-version: '3.13'
           cache: 'pip'
           cache: 'pip'
       - name: install python packages
       - name: install python packages
         run: pip install -r main-repo-triage/requirements.txt
         run: pip install -r main-repo-triage/requirements.txt

+ 1 - 1
.github/workflows/pull-request-conflict.yml

@@ -12,7 +12,7 @@ jobs:
   label:
   label:
     name: Labeling
     name: Labeling
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
-    if: ${{ github.repository == 'jellyfin/jellyfin' }}
+    if: ${{ github.repository == 'jellyfin/jellyfin' && github.event.issue.pull_request }}
     steps:
     steps:
       - name: Apply label
       - name: Apply label
         uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3
         uses: eps1lon/actions-label-merge-conflict@1df065ebe6e3310545d4f4c4e862e43bdca146f0 # v3.0.3

+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "dotnet.preferVisualStudioCodeFileSystemWatcher": true
+}

+ 8 - 0
CONTRIBUTORS.md

@@ -27,6 +27,7 @@
  - [cryptobank](https://github.com/cryptobank)
  - [cryptobank](https://github.com/cryptobank)
  - [cvium](https://github.com/cvium)
  - [cvium](https://github.com/cvium)
  - [dannymichel](https://github.com/dannymichel)
  - [dannymichel](https://github.com/dannymichel)
+ - [darioackermann](https://github.com/darioackermann)
  - [DaveChild](https://github.com/DaveChild)
  - [DaveChild](https://github.com/DaveChild)
  - [DavidFair](https://github.com/DavidFair)
  - [DavidFair](https://github.com/DavidFair)
  - [Delgan](https://github.com/Delgan)
  - [Delgan](https://github.com/Delgan)
@@ -60,6 +61,7 @@
  - [ikomhoog](https://github.com/ikomhoog)
  - [ikomhoog](https://github.com/ikomhoog)
  - [iwalton3](https://github.com/iwalton3)
  - [iwalton3](https://github.com/iwalton3)
  - [jftuga](https://github.com/jftuga)
  - [jftuga](https://github.com/jftuga)
+ - [jkhsjdhjs](https://github.com/jkhsjdhjs)
  - [jmshrv](https://github.com/jmshrv)
  - [jmshrv](https://github.com/jmshrv)
  - [joern-h](https://github.com/joern-h)
  - [joern-h](https://github.com/joern-h)
  - [joshuaboniface](https://github.com/joshuaboniface)
  - [joshuaboniface](https://github.com/joshuaboniface)
@@ -196,6 +198,10 @@
  - [benedikt257](https://github.com/benedikt257)
  - [benedikt257](https://github.com/benedikt257)
  - [revam](https://github.com/revam)
  - [revam](https://github.com/revam)
  - [Jxiced](https://github.com/Jxiced)
  - [Jxiced](https://github.com/Jxiced)
+ - [allesmi](https://github.com/allesmi)
+ - [ThunderClapLP](https://github.com/ThunderClapLP)
+ - [Shoham Peller](https://github.com/spellr)
+ - [theshoeshiner](https://github.com/theshoeshiner)
 
 
 # Emby Contributors
 # Emby Contributors
 
 
@@ -270,3 +276,5 @@
  - [Robert Lützner](https://github.com/rluetzner)
  - [Robert Lützner](https://github.com/rluetzner)
  - [Nathan McCrina](https://github.com/nfmccrina)
  - [Nathan McCrina](https://github.com/nfmccrina)
  - [Martin Reuter](https://github.com/reuterma24)
  - [Martin Reuter](https://github.com/reuterma24)
+ - [Michael McElroy](https://github.com/mcmcelro)
+ - [Soumyadip Auddy](https://github.com/SoumyadipAuddy)

+ 49 - 43
Directory.Packages.props

@@ -4,82 +4,88 @@
   </PropertyGroup>
   </PropertyGroup>
   <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
   <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
   <ItemGroup Label="Package Dependencies">
   <ItemGroup Label="Package Dependencies">
-    <PackageVersion Include="AsyncKeyedLock" Version="7.1.4" />
+    <PackageVersion Include="AsyncKeyedLock" Version="7.1.6" />
     <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
     <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.1" />
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.1" />
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <PackageVersion Include="AutoFixture" Version="4.18.1" />
     <PackageVersion Include="BDInfo" Version="0.8.0" />
     <PackageVersion Include="BDInfo" Version="0.8.0" />
-    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
-    <PackageVersion Include="BlurHashSharp" Version="1.3.4" />
+    <PackageVersion Include="BitFaster.Caching" Version="2.5.4" />
+    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.4.0-pre.1" />
+    <PackageVersion Include="BlurHashSharp" Version="1.4.0-pre.1" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="CommandLineParser" Version="2.9.1" />
     <PackageVersion Include="coverlet.collector" Version="6.0.4" />
     <PackageVersion Include="coverlet.collector" Version="6.0.4" />
-    <PackageVersion Include="Diacritics" Version="3.3.29" />
+    <PackageVersion Include="Diacritics" Version="4.0.17" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
     <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
-    <PackageVersion Include="FsCheck.Xunit" Version="3.1.0" />
-    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.3" />
+    <PackageVersion Include="FsCheck.Xunit" Version="3.3.0" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="8.3.1.1" />
     <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
     <PackageVersion Include="ICU4N.Transliterator" Version="60.1.0-alpha.356" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
     <PackageVersion Include="IDisposableAnalyzers" Version="4.0.8" />
+    <PackageVersion Include="Ignore" Version="0.2.1" />
     <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
     <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
-    <PackageVersion Include="libse" Version="4.0.10" />
-    <PackageVersion Include="LrcParser" Version="2024.0728.2" />
+    <PackageVersion Include="libse" Version="4.0.12" />
+    <PackageVersion Include="LrcParser" Version="2025.623.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
     <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
-    <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
-    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0" />
+    <PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.7" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
     <PackageVersion Include="MimeTypes" Version="2.5.2" />
     <PackageVersion Include="MimeTypes" Version="2.5.2" />
+    <PackageVersion Include="Morestachio" Version="5.0.1.631" />
     <PackageVersion Include="Moq" Version="4.18.4" />
     <PackageVersion Include="Moq" Version="4.18.4" />
-    <PackageVersion Include="NEbml" Version="0.12.0" />
+    <PackageVersion Include="NEbml" Version="1.0.0.3" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
     <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
     <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
     <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
     <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
     <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
     <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
     <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
     <PackageVersion Include="prometheus-net" Version="8.2.1" />
     <PackageVersion Include="prometheus-net" Version="8.2.1" />
+    <PackageVersion Include="Polly" Version="8.6.2" />
     <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
     <PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
     <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
     <PackageVersion Include="Serilog.Enrichers.Thread" Version="4.0.0" />
+    <PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
     <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
     <PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
     <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
     <PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
     <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
     <PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
-    <PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
+    <PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
     <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
     <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
     <PackageVersion Include="SharpFuzz" Version="2.2.0" />
     <PackageVersion Include="SharpFuzz" Version="2.2.0" />
-    <PackageVersion Include="SkiaSharp" Version="2.88.9" />
-    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.9" />
-    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
+     <!-- Pinned to 3.116.1 because https://github.com/jellyfin/jellyfin/pull/14255 -->
+    <PackageVersion Include="SkiaSharp" Version="3.116.1" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.116.1" />
+    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
-    <PackageVersion Include="Svg.Skia" Version="2.0.0.4" />
+    <PackageVersion Include="Svg.Skia" Version="3.0.4" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
     <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
     <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
     <PackageVersion Include="System.Globalization" Version="4.3.0" />
     <PackageVersion Include="System.Globalization" Version="4.3.0" />
-    <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
-    <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
-    <PackageVersion Include="System.Text.Json" Version="9.0.2" />
-    <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.2" />
+    <PackageVersion Include="System.Linq.Async" Version="6.0.3" />
+    <PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
+    <PackageVersion Include="System.Text.Json" Version="9.0.7" />
+    <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="9.0.7" />
     <PackageVersion Include="TagLibSharp" Version="2.3.0" />
     <PackageVersion Include="TagLibSharp" Version="2.3.0" />
-    <PackageVersion Include="z440.atl.core" Version="6.17.0" />
+    <PackageVersion Include="z440.atl.core" Version="7.2.0" />
     <PackageVersion Include="TMDbLib" Version="2.2.0" />
     <PackageVersion Include="TMDbLib" Version="2.2.0" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
     <PackageVersion Include="Xunit.Priority" Version="1.1.6" />

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

@@ -187,7 +187,9 @@ namespace Emby.Naming.Common
                 "disc",
                 "disc",
                 "disk",
                 "disk",
                 "vol",
                 "vol",
-                "volume"
+                "volume",
+                "part",
+                "act"
             };
             };
 
 
             ArtistSubfolders = new[]
             ArtistSubfolders = new[]
@@ -238,6 +240,7 @@ namespace Emby.Naming.Common
                 ".dsp",
                 ".dsp",
                 ".dts",
                 ".dts",
                 ".dvf",
                 ".dvf",
+                ".eac3",
                 ".far",
                 ".far",
                 ".flac",
                 ".flac",
                 ".gdm",
                 ".gdm",

+ 38 - 52
Emby.Naming/TV/SeasonPathParser.cs

@@ -1,43 +1,35 @@
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
+using System.Text.RegularExpressions;
 
 
 namespace Emby.Naming.TV
 namespace Emby.Naming.TV
 {
 {
     /// <summary>
     /// <summary>
     /// Class to parse season paths.
     /// Class to parse season paths.
     /// </summary>
     /// </summary>
-    public static class SeasonPathParser
+    public static partial class SeasonPathParser
     {
     {
-        /// <summary>
-        /// A season folder must contain one of these somewhere in the name.
-        /// </summary>
-        private static readonly string[] _seasonFolderNames =
-        {
-            "season",
-            "sæson",
-            "temporada",
-            "saison",
-            "staffel",
-            "series",
-            "сезон",
-            "stagione"
-        };
-
-        private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
+        [GeneratedRegex(@"^\s*((?<seasonnumber>(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<rightpart>.*)$")]
+        private static partial Regex ProcessPre();
+
+        [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?<seasonnumber>(?>\d+)(?!\s*[Ee]\d+))(?<rightpart>.*)$")]
+        private static partial Regex ProcessPost();
 
 
         /// <summary>
         /// <summary>
         /// Attempts to parse season number from path.
         /// Attempts to parse season number from path.
         /// </summary>
         /// </summary>
         /// <param name="path">Path to season.</param>
         /// <param name="path">Path to season.</param>
+        /// <param name="parentPath">Folder name of the parent.</param>
         /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
         /// <param name="supportSpecialAliases">Support special aliases when parsing.</param>
         /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
         /// <param name="supportNumericSeasonFolders">Support numeric season folders when parsing.</param>
         /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
         /// <returns>Returns <see cref="SeasonPathParserResult"/> object.</returns>
-        public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders)
+        public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders)
         {
         {
             var result = new SeasonPathParserResult();
             var result = new SeasonPathParserResult();
+            var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name;
 
 
-            var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders);
+            var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders);
 
 
             result.SeasonNumber = seasonNumber;
             result.SeasonNumber = seasonNumber;
 
 
@@ -54,15 +46,24 @@ namespace Emby.Naming.TV
         /// Gets the season number from path.
         /// Gets the season number from path.
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
+        /// <param name="parentFolderName">The parent folder name.</param>
         /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
         /// <param name="supportSpecialAliases">if set to <c>true</c> [support special aliases].</param>
         /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
         /// <param name="supportNumericSeasonFolders">if set to <c>true</c> [support numeric season folders].</param>
         /// <returns>System.Nullable{System.Int32}.</returns>
         /// <returns>System.Nullable{System.Int32}.</returns>
         private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
         private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath(
             string path,
             string path,
+            string? parentFolderName,
             bool supportSpecialAliases,
             bool supportSpecialAliases,
             bool supportNumericSeasonFolders)
             bool supportNumericSeasonFolders)
         {
         {
             string filename = Path.GetFileName(path);
             string filename = Path.GetFileName(path);
+            filename = Regex.Replace(filename, "[ ._-]", string.Empty);
+
+            if (parentFolderName is not null)
+            {
+                parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty);
+                filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase);
+            }
 
 
             if (supportSpecialAliases)
             if (supportSpecialAliases)
             {
             {
@@ -85,53 +86,38 @@ namespace Emby.Naming.TV
                 }
                 }
             }
             }
 
 
-            if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
+            if (filename.StartsWith('s'))
             {
             {
-                return (seasonNumber, true);
-            }
+                var testFilename = filename.AsSpan()[1..];
 
 
-            // Look for one of the season folder names
-            foreach (var name in _seasonFolderNames)
-            {
-                if (filename.Contains(name, StringComparison.OrdinalIgnoreCase))
+                if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                 {
                 {
-                    var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase));
-                    if (result.SeasonNumber.HasValue)
-                    {
-                        return result;
-                    }
-
-                    break;
+                    return (val, true);
                 }
                 }
             }
             }
 
 
-            var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
-            foreach (var part in parts)
+            var preMatch = ProcessPre().Match(filename);
+            if (preMatch.Success)
             {
             {
-                if (TryGetSeasonNumberFromPart(part, out seasonNumber))
-                {
-                    return (seasonNumber, true);
-                }
+                return CheckMatch(preMatch);
             }
             }
-
-            return (null, true);
-        }
-
-        private static bool TryGetSeasonNumberFromPart(ReadOnlySpan<char> part, out int seasonNumber)
-        {
-            seasonNumber = 0;
-            if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase))
+            else
             {
             {
-                return false;
+                var postMatch = ProcessPost().Match(filename);
+                return CheckMatch(postMatch);
             }
             }
+        }
 
 
-            if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
+        private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match)
+        {
+            var numberString = match.Groups["seasonnumber"];
+            if (numberString.Success)
             {
             {
-                seasonNumber = value;
-                return true;
+                var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture);
+                return (seasonNumber, true);
             }
             }
 
 
-            return false;
+            return (null, false);
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 5 - 2
Emby.Naming/Video/ExtraRuleResolver.cs

@@ -18,8 +18,9 @@ namespace Emby.Naming.Video
         /// </summary>
         /// </summary>
         /// <param name="path">Path to file.</param>
         /// <param name="path">Path to file.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
         /// <returns>Returns <see cref="ExtraResult"/> object.</returns>
-        public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions)
+        public static ExtraResult GetExtraInfo(string path, NamingOptions namingOptions, string? libraryRoot = "")
         {
         {
             var result = new ExtraResult();
             var result = new ExtraResult();
 
 
@@ -69,7 +70,9 @@ namespace Emby.Naming.Video
                 else if (rule.RuleType == ExtraRuleType.DirectoryName)
                 else if (rule.RuleType == ExtraRuleType.DirectoryName)
                 {
                 {
                     var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
                     var directoryName = Path.GetFileName(Path.GetDirectoryName(pathSpan));
-                    if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
+                    string fullDirectory = Path.GetDirectoryName(pathSpan).ToString();
+                    if (directoryName.Equals(rule.Token, StringComparison.OrdinalIgnoreCase)
+                        && !string.Equals(fullDirectory, libraryRoot, StringComparison.OrdinalIgnoreCase))
                     {
                     {
                         result.ExtraType = rule.ExtraType;
                         result.ExtraType = rule.ExtraType;
                         result.Rule = rule;
                         result.Rule = rule;

+ 3 - 2
Emby.Naming/Video/VideoListResolver.cs

@@ -27,8 +27,9 @@ namespace Emby.Naming.Video
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
         /// <param name="supportMultiVersion">Indication we should consider multi-versions of content.</param>
         /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <param name="parseName">Whether to parse the name or use the filename.</param>
+        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
         /// <returns>Returns enumerable of <see cref="VideoInfo"/> which groups files together when related.</returns>
-        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true)
+        public static IReadOnlyList<VideoInfo> Resolve(IReadOnlyList<VideoFileInfo> videoInfos, NamingOptions namingOptions, bool supportMultiVersion = true, bool parseName = true, string? libraryRoot = "")
         {
         {
             // Filter out all extras, otherwise they could cause stacks to not be resolved
             // Filter out all extras, otherwise they could cause stacks to not be resolved
             // See the unit test TestStackedWithTrailer
             // See the unit test TestStackedWithTrailer
@@ -65,7 +66,7 @@ namespace Emby.Naming.Video
             {
             {
                 var info = new VideoInfo(stack.Name)
                 var info = new VideoInfo(stack.Name)
                 {
                 {
-                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName))
+                    Files = stack.Files.Select(i => VideoResolver.Resolve(i, stack.IsDirectoryStack, namingOptions, parseName, libraryRoot))
                         .OfType<VideoFileInfo>()
                         .OfType<VideoFileInfo>()
                         .ToList()
                         .ToList()
                 };
                 };

+ 9 - 6
Emby.Naming/Video/VideoResolver.cs

@@ -17,10 +17,11 @@ namespace Emby.Naming.Video
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="parseName">Whether to parse the name or use the filename.</param>
         /// <param name="parseName">Whether to parse the name or use the filename.</param>
+        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
         /// <returns>VideoFileInfo.</returns>
         /// <returns>VideoFileInfo.</returns>
-        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true)
+        public static VideoFileInfo? ResolveDirectory(string? path, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
         {
         {
-            return Resolve(path, true, namingOptions, parseName);
+            return Resolve(path, true, namingOptions, parseName, libraryRoot);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -28,10 +29,11 @@ namespace Emby.Naming.Video
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
         /// <returns>VideoFileInfo.</returns>
         /// <returns>VideoFileInfo.</returns>
-        public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions)
+        public static VideoFileInfo? ResolveFile(string? path, NamingOptions namingOptions, string? libraryRoot = "")
         {
         {
-            return Resolve(path, false, namingOptions);
+            return Resolve(path, false, namingOptions, libraryRoot: libraryRoot);
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -41,9 +43,10 @@ namespace Emby.Naming.Video
         /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
         /// <param name="isDirectory">if set to <c>true</c> [is folder].</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="parseName">Whether or not the name should be parsed for info.</param>
         /// <param name="parseName">Whether or not the name should be parsed for info.</param>
+        /// <param name="libraryRoot">Top-level folder for the containing library.</param>
         /// <returns>VideoFileInfo.</returns>
         /// <returns>VideoFileInfo.</returns>
         /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
         /// <exception cref="ArgumentNullException"><c>path</c> is <c>null</c>.</exception>
-        public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true)
+        public static VideoFileInfo? Resolve(string? path, bool isDirectory, NamingOptions namingOptions, bool parseName = true, string? libraryRoot = "")
         {
         {
             if (string.IsNullOrEmpty(path))
             if (string.IsNullOrEmpty(path))
             {
             {
@@ -75,7 +78,7 @@ namespace Emby.Naming.Video
 
 
             var format3DResult = Format3DParser.Parse(path, namingOptions);
             var format3DResult = Format3DParser.Parse(path, namingOptions);
 
 
-            var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions);
+            var extraResult = ExtraRuleResolver.GetExtraInfo(path, namingOptions, libraryRoot);
 
 
             var name = Path.GetFileNameWithoutExtension(path);
             var name = Path.GetFileNameWithoutExtension(path);
 
 

+ 2 - 2
Emby.Photos/PhotoProvider.cs

@@ -49,7 +49,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
         if (item.IsFileProtocol)
         if (item.IsFileProtocol)
         {
         {
             var file = directoryService.GetFile(item.Path);
             var file = directoryService.GetFile(item.Path);
-            return file is not null && file.LastWriteTimeUtc != item.DateModified;
+            return file is not null && item.HasChanged(file.LastWriteTimeUtc);
         }
         }
 
 
         return false;
         return false;
@@ -108,7 +108,7 @@ public class PhotoProvider : ICustomMetadataProvider<Photo>, IForcedProvider, IH
                     var dateTaken = image.ImageTag.DateTime;
                     var dateTaken = image.ImageTag.DateTime;
                     if (dateTaken.HasValue)
                     if (dateTaken.HasValue)
                     {
                     {
-                        item.DateCreated = dateTaken.Value;
+                        item.DateCreated = dateTaken.Value.ToUniversalTime();
                         item.PremiereDate = dateTaken.Value;
                         item.PremiereDate = dateTaken.Value;
                         item.ProductionYear = dateTaken.Value.Year;
                         item.ProductionYear = dateTaken.Value.Year;
                     }
                     }

+ 59 - 45
Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs

@@ -1,5 +1,8 @@
 using System;
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.IO;
+using System.Linq;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 
 
 namespace Emby.Server.Implementations.AppBase
 namespace Emby.Server.Implementations.AppBase
@@ -30,80 +33,91 @@ namespace Emby.Server.Implementations.AppBase
             ConfigurationDirectoryPath = configurationDirectoryPath;
             ConfigurationDirectoryPath = configurationDirectoryPath;
             CachePath = cacheDirectoryPath;
             CachePath = cacheDirectoryPath;
             WebPath = webDirectoryPath;
             WebPath = webDirectoryPath;
-
             DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
             DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
         }
         }
 
 
-        /// <summary>
-        /// Gets the path to the program data folder.
-        /// </summary>
-        /// <value>The program data path.</value>
+        /// <inheritdoc/>
         public string ProgramDataPath { get; }
         public string ProgramDataPath { get; }
 
 
         /// <inheritdoc/>
         /// <inheritdoc/>
         public string WebPath { get; }
         public string WebPath { get; }
 
 
-        /// <summary>
-        /// Gets the path to the system folder.
-        /// </summary>
-        /// <value>The path to the system folder.</value>
+        /// <inheritdoc/>
         public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
         public string ProgramSystemPath { get; } = AppContext.BaseDirectory;
 
 
-        /// <summary>
-        /// Gets the folder path to the data directory.
-        /// </summary>
-        /// <value>The data directory.</value>
+        /// <inheritdoc/>
         public string DataPath { get; }
         public string DataPath { get; }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string VirtualDataPath => "%AppDataPath%";
         public string VirtualDataPath => "%AppDataPath%";
 
 
-        /// <summary>
-        /// Gets the image cache path.
-        /// </summary>
-        /// <value>The image cache path.</value>
+        /// <inheritdoc/>
         public string ImageCachePath => Path.Combine(CachePath, "images");
         public string ImageCachePath => Path.Combine(CachePath, "images");
 
 
-        /// <summary>
-        /// Gets the path to the plugin directory.
-        /// </summary>
-        /// <value>The plugins path.</value>
+        /// <inheritdoc/>
         public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
         public string PluginsPath => Path.Combine(ProgramDataPath, "plugins");
 
 
-        /// <summary>
-        /// Gets the path to the plugin configurations directory.
-        /// </summary>
-        /// <value>The plugin configurations path.</value>
+        /// <inheritdoc/>
         public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
         public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations");
 
 
-        /// <summary>
-        /// Gets the path to the log directory.
-        /// </summary>
-        /// <value>The log directory path.</value>
+        /// <inheritdoc/>
         public string LogDirectoryPath { get; }
         public string LogDirectoryPath { get; }
 
 
-        /// <summary>
-        /// Gets the path to the application configuration root directory.
-        /// </summary>
-        /// <value>The configuration directory path.</value>
+        /// <inheritdoc/>
         public string ConfigurationDirectoryPath { get; }
         public string ConfigurationDirectoryPath { get; }
 
 
-        /// <summary>
-        /// Gets the path to the system configuration file.
-        /// </summary>
-        /// <value>The system configuration file path.</value>
+        /// <inheritdoc/>
         public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
         public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml");
 
 
-        /// <summary>
-        /// Gets or sets the folder path to the cache directory.
-        /// </summary>
-        /// <value>The cache directory.</value>
+        /// <inheritdoc/>
         public string CachePath { get; set; }
         public string CachePath { get; set; }
 
 
-        /// <summary>
-        /// Gets the folder path to the temp directory within the cache folder.
-        /// </summary>
-        /// <value>The temp directory.</value>
+        /// <inheritdoc/>
         public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
         public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin");
+
+        /// <inheritdoc />
+        public string TrickplayPath => Path.Combine(DataPath, "trickplay");
+
+        /// <inheritdoc />
+        public string BackupPath => Path.Combine(DataPath, "backups");
+
+        /// <inheritdoc />
+        public virtual void MakeSanityCheckOrThrow()
+        {
+            CreateAndCheckMarker(ConfigurationDirectoryPath, "config");
+            CreateAndCheckMarker(LogDirectoryPath, "log");
+            CreateAndCheckMarker(PluginsPath, "plugin");
+            CreateAndCheckMarker(ProgramDataPath, "data");
+            CreateAndCheckMarker(CachePath, "cache");
+            CreateAndCheckMarker(DataPath, "data");
+        }
+
+        /// <inheritdoc />
+        public void CreateAndCheckMarker(string path, string markerName, bool recursive = false)
+        {
+            Directory.CreateDirectory(path);
+
+            CheckOrCreateMarker(path, $".jellyfin-{markerName}", recursive);
+        }
+
+        private IEnumerable<string> GetMarkers(string path, bool recursive = false)
+        {
+            return Directory.EnumerateFiles(path, ".jellyfin-*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly);
+        }
+
+        private void CheckOrCreateMarker(string path, string markerName, bool recursive = false)
+        {
+            var otherMarkers = GetMarkers(path, recursive).FirstOrDefault(e => Path.GetFileName(e) != markerName);
+            if (otherMarkers != null)
+            {
+                throw new InvalidOperationException($"Exepected to find only {markerName} but found marker for {otherMarkers}.");
+            }
+
+            var markerPath = Path.Combine(path, markerName);
+            if (!File.Exists(markerPath))
+            {
+                FileHelper.CreateEmpty(markerPath);
+            }
+        }
     }
     }
 }
 }

+ 1 - 0
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -227,6 +227,7 @@ namespace Emby.Server.Implementations.AppBase
 
 
             Logger.LogInformation("Setting cache path: {Path}", cachePath);
             Logger.LogInformation("Setting cache path: {Path}", cachePath);
             ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
             ((BaseApplicationPaths)CommonApplicationPaths).CachePath = cachePath;
+            CommonApplicationPaths.CreateAndCheckMarker(((BaseApplicationPaths)CommonApplicationPaths).CachePath, "cache");
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 32 - 27
Emby.Server.Implementations/ApplicationHost.cs

@@ -15,6 +15,7 @@ using System.Security.Cryptography.X509Certificates;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 using Emby.Photos;
 using Emby.Photos;
+using Emby.Server.Implementations.Chapters;
 using Emby.Server.Implementations.Collections;
 using Emby.Server.Implementations.Collections;
 using Emby.Server.Implementations.Configuration;
 using Emby.Server.Implementations.Configuration;
 using Emby.Server.Implementations.Cryptography;
 using Emby.Server.Implementations.Cryptography;
@@ -39,9 +40,10 @@ using Jellyfin.Drawing;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.MediaEncoding.Hls.Playlist;
 using Jellyfin.Networking.Manager;
 using Jellyfin.Networking.Manager;
 using Jellyfin.Networking.Udp;
 using Jellyfin.Networking.Udp;
-using Jellyfin.Server.Implementations;
+using Jellyfin.Server.Implementations.FullSystemBackup;
 using Jellyfin.Server.Implementations.Item;
 using Jellyfin.Server.Implementations.Item;
 using Jellyfin.Server.Implementations.MediaSegments;
 using Jellyfin.Server.Implementations.MediaSegments;
+using Jellyfin.Server.Implementations.SystemBackupService;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Events;
@@ -57,10 +59,14 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.LibraryTaskScheduler;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.MediaSegments;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
@@ -91,7 +97,6 @@ using MediaBrowser.Providers.Subtitles;
 using MediaBrowser.XbmcMetadata.Providers;
 using MediaBrowser.XbmcMetadata.Providers;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -266,6 +271,8 @@ namespace Emby.Server.Implementations
                 ? Environment.MachineName
                 ? Environment.MachineName
                 : ConfigurationManager.Configuration.ServerName;
                 : ConfigurationManager.Configuration.ServerName;
 
 
+        public string RestoreBackupPath { get; set; }
+
         public string ExpandVirtualPath(string path)
         public string ExpandVirtualPath(string path)
         {
         {
             if (path is null)
             if (path is null)
@@ -470,6 +477,7 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IApplicationHost>(this);
             serviceCollection.AddSingleton<IApplicationHost>(this);
             serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
             serviceCollection.AddSingleton<IPluginManager>(_pluginManager);
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+            serviceCollection.AddSingleton<IBackupService, BackupService>();
 
 
             serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
             serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
             serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
             serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
@@ -504,10 +512,13 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
             serviceCollection.AddSingleton<IChapterRepository, ChapterRepository>();
             serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
             serviceCollection.AddSingleton<IMediaAttachmentRepository, MediaAttachmentRepository>();
             serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
             serviceCollection.AddSingleton<IMediaStreamRepository, MediaStreamRepository>();
+            serviceCollection.AddSingleton<IKeyframeRepository, KeyframeRepository>();
             serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
             serviceCollection.AddSingleton<IItemTypeLookup, ItemTypeLookup>();
 
 
             serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
             serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
             serviceCollection.AddSingleton<EncodingHelper>();
             serviceCollection.AddSingleton<EncodingHelper>();
+            serviceCollection.AddSingleton<IPathManager, PathManager>();
+            serviceCollection.AddSingleton<IExternalDataManager, ExternalDataManager>();
 
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
             serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
             serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
@@ -542,6 +553,7 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
 
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+            serviceCollection.AddSingleton<ILimitedConcurrencyLibraryScheduler, LimitedConcurrencyLibraryScheduler>();
 
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
 
@@ -549,13 +561,14 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
 
-            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
 
             serviceCollection.AddSingleton<IAuthService, AuthService>();
             serviceCollection.AddSingleton<IAuthService, AuthService>();
             serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
             serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 
 
             serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
             serviceCollection.AddSingleton<ISubtitleParser, SubtitleEditParser>();
             serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
             serviceCollection.AddSingleton<ISubtitleEncoder, SubtitleEncoder>();
+            serviceCollection.AddSingleton<IKeyframeManager, KeyframeManager>();
 
 
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
             serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
 
@@ -572,20 +585,10 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Create services registered with the service container that need to be initialized at application startup.
         /// Create services registered with the service container that need to be initialized at application startup.
         /// </summary>
         /// </summary>
+        /// <param name="startupConfig">The configuration used to initialise the application.</param>
         /// <returns>A task representing the service initialization operation.</returns>
         /// <returns>A task representing the service initialization operation.</returns>
-        public async Task InitializeServices()
+        public async Task InitializeServices(IConfiguration startupConfig)
         {
         {
-            var jellyfinDb = await Resolve<IDbContextFactory<JellyfinDbContext>>().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>();
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
             await localizationManager.LoadAll().ConfigureAwait(false);
 
 
@@ -633,24 +636,26 @@ namespace Emby.Server.Implementations
         private void SetStaticProperties()
         private void SetStaticProperties()
         {
         {
             // For now there's no real way to inject these properly
             // For now there's no real way to inject these properly
-            BaseItem.Logger = Resolve<ILogger<BaseItem>>();
+            BaseItem.ChapterManager = Resolve<IChapterManager>();
+            BaseItem.ChannelManager = Resolve<IChannelManager>();
             BaseItem.ConfigurationManager = ConfigurationManager;
             BaseItem.ConfigurationManager = ConfigurationManager;
+            BaseItem.FileSystem = Resolve<IFileSystem>();
+            BaseItem.ItemRepository = Resolve<IItemRepository>();
             BaseItem.LibraryManager = Resolve<ILibraryManager>();
             BaseItem.LibraryManager = Resolve<ILibraryManager>();
-            BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
-            BaseItem.ItemRepository = Resolve<IItemRepository>();
-            BaseItem.ChapterRepository = Resolve<IChapterRepository>();
-            BaseItem.FileSystem = Resolve<IFileSystem>();
-            BaseItem.UserDataManager = Resolve<IUserDataManager>();
-            BaseItem.ChannelManager = Resolve<IChannelManager>();
-            Video.RecordingsManager = Resolve<IRecordingsManager>();
-            Folder.UserViewManager = Resolve<IUserViewManager>();
-            UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
-            UserView.CollectionManager = Resolve<ICollectionManager>();
-            BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+            BaseItem.Logger = Resolve<ILogger<BaseItem>>();
             BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
             BaseItem.MediaSegmentManager = Resolve<IMediaSegmentManager>();
+            BaseItem.MediaSourceManager = Resolve<IMediaSourceManager>();
+            BaseItem.ProviderManager = Resolve<IProviderManager>();
+            BaseItem.UserDataManager = Resolve<IUserDataManager>();
             CollectionFolder.XmlSerializer = _xmlSerializer;
             CollectionFolder.XmlSerializer = _xmlSerializer;
             CollectionFolder.ApplicationHost = this;
             CollectionFolder.ApplicationHost = this;
+            Folder.UserViewManager = Resolve<IUserViewManager>();
+            Folder.CollectionManager = Resolve<ICollectionManager>();
+            Folder.LimitedConcurrencyLibraryScheduler = Resolve<ILimitedConcurrencyLibraryScheduler>();
+            Episode.MediaEncoder = Resolve<IMediaEncoder>();
+            UserView.TVSeriesManager = Resolve<ITVSeriesManager>();
+            Video.RecordingsManager = Resolve<IRecordingsManager>();
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 313 - 0
Emby.Server.Implementations/Chapters/ChapterManager.cs

@@ -0,0 +1,313 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.Extensions;
+using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.MediaEncoding;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Dto;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Chapters;
+
+/// <summary>
+/// The chapter manager.
+/// </summary>
+public class ChapterManager : IChapterManager
+{
+    private readonly IFileSystem _fileSystem;
+    private readonly ILogger<ChapterManager> _logger;
+    private readonly IMediaEncoder _encoder;
+    private readonly IChapterRepository _chapterRepository;
+    private readonly ILibraryManager _libraryManager;
+    private readonly IPathManager _pathManager;
+
+    /// <summary>
+    /// The first chapter ticks.
+    /// </summary>
+    private static readonly long _firstChapterTicks = TimeSpan.FromSeconds(15).Ticks;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ChapterManager"/> class.
+    /// </summary>
+    /// <param name="logger">The <see cref="ILogger{ChapterManager}"/>.</param>
+    /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param>
+    /// <param name="encoder">The <see cref="IMediaEncoder"/>.</param>
+    /// <param name="chapterRepository">The <see cref="IChapterRepository"/>.</param>
+    /// <param name="libraryManager">The <see cref="ILibraryManager"/>.</param>
+    /// <param name="pathManager">The <see cref="IPathManager"/>.</param>
+    public ChapterManager(
+        ILogger<ChapterManager> logger,
+        IFileSystem fileSystem,
+        IMediaEncoder encoder,
+        IChapterRepository chapterRepository,
+        ILibraryManager libraryManager,
+        IPathManager pathManager)
+    {
+        _logger = logger;
+        _fileSystem = fileSystem;
+        _encoder = encoder;
+        _chapterRepository = chapterRepository;
+        _libraryManager = libraryManager;
+        _pathManager = pathManager;
+    }
+
+    /// <summary>
+    /// Determines whether [is eligible for chapter image extraction] [the specified video].
+    /// </summary>
+    /// <param name="video">The video.</param>
+    /// <param name="libraryOptions">The library options for the video.</param>
+    /// <returns><c>true</c> if [is eligible for chapter image extraction] [the specified video]; otherwise, <c>false</c>.</returns>
+    private bool IsEligibleForChapterImageExtraction(Video video, LibraryOptions libraryOptions)
+    {
+        if (video.IsPlaceHolder)
+        {
+            return false;
+        }
+
+        if (libraryOptions is null || !libraryOptions.EnableChapterImageExtraction)
+        {
+            return false;
+        }
+
+        if (video.IsShortcut)
+        {
+            return false;
+        }
+
+        if (!video.IsCompleteMedia)
+        {
+            return false;
+        }
+
+        // Can't extract images if there are no video streams
+        return video.DefaultVideoStreamIndex.HasValue;
+    }
+
+    private long GetAverageDurationBetweenChapters(IReadOnlyList<ChapterInfo> chapters)
+    {
+        if (chapters.Count < 2)
+        {
+            return 0;
+        }
+
+        long sum = 0;
+        for (int i = 1; i < chapters.Count; i++)
+        {
+            sum += chapters[i].StartPositionTicks - chapters[i - 1].StartPositionTicks;
+        }
+
+        return sum / chapters.Count;
+    }
+
+    /// <inheritdoc />
+    public async Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken)
+    {
+        if (chapters.Count == 0)
+        {
+            return true;
+        }
+
+        var libraryOptions = _libraryManager.GetLibraryOptions(video);
+
+        if (!IsEligibleForChapterImageExtraction(video, libraryOptions))
+        {
+            extractImages = false;
+        }
+
+        var averageChapterDuration = GetAverageDurationBetweenChapters(chapters);
+        var threshold = TimeSpan.FromSeconds(1).Ticks;
+        if (averageChapterDuration < threshold)
+        {
+            _logger.LogInformation("Skipping chapter image extraction for {Video} as the average chapter duration {AverageDuration} was lower than the minimum threshold {Threshold}", video.Name, averageChapterDuration, threshold);
+            extractImages = false;
+        }
+
+        var success = true;
+        var changesMade = false;
+
+        var runtimeTicks = video.RunTimeTicks ?? 0;
+
+        var currentImages = GetSavedChapterImages(video, directoryService);
+
+        foreach (var chapter in chapters)
+        {
+            if (chapter.StartPositionTicks >= runtimeTicks)
+            {
+                _logger.LogInformation("Stopping chapter extraction for {0} because a chapter was found with a position greater than the runtime.", video.Name);
+                break;
+            }
+
+            var path = _pathManager.GetChapterImagePath(video, chapter.StartPositionTicks);
+
+            if (!currentImages.Contains(path, StringComparison.OrdinalIgnoreCase))
+            {
+                if (extractImages)
+                {
+                    cancellationToken.ThrowIfCancellationRequested();
+
+                    try
+                    {
+                        // Add some time for the first chapter to make sure we don't end up with a black image
+                        var time = chapter.StartPositionTicks == 0 ? TimeSpan.FromTicks(Math.Min(_firstChapterTicks, video.RunTimeTicks ?? 0)) : TimeSpan.FromTicks(chapter.StartPositionTicks);
+
+                        var inputPath = video.Path;
+                        var directoryPath = Path.GetDirectoryName(path);
+                        if (!string.IsNullOrEmpty(directoryPath))
+                        {
+                            Directory.CreateDirectory(directoryPath);
+                        }
+
+                        var container = video.Container;
+                        var mediaSource = new MediaSourceInfo
+                        {
+                            VideoType = video.VideoType,
+                            IsoType = video.IsoType,
+                            Protocol = video.PathProtocol ?? MediaProtocol.File,
+                        };
+
+                        _logger.LogInformation("Extracting chapter image for {Name} at {Path}", video.Name, inputPath);
+                        var tempFile = await _encoder.ExtractVideoImage(inputPath, container, mediaSource, video.GetDefaultVideoStream(), video.Video3DFormat, time, cancellationToken).ConfigureAwait(false);
+                        File.Copy(tempFile, path, true);
+
+                        try
+                        {
+                            _fileSystem.DeleteFile(tempFile);
+                        }
+                        catch (IOException ex)
+                        {
+                            _logger.LogError(ex, "Error deleting temporary chapter image encoding file {Path}", tempFile);
+                        }
+
+                        chapter.ImagePath = path;
+                        chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
+                        changesMade = true;
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.LogError(ex, "Error extracting chapter images for {0}", string.Join(',', video.Path));
+                        success = false;
+                        break;
+                    }
+                }
+                else if (!string.IsNullOrEmpty(chapter.ImagePath))
+                {
+                    chapter.ImagePath = null;
+                    changesMade = true;
+                }
+            }
+            else if (!string.Equals(path, chapter.ImagePath, StringComparison.OrdinalIgnoreCase))
+            {
+                chapter.ImagePath = path;
+                chapter.ImageDateModified = _fileSystem.GetLastWriteTimeUtc(path);
+                changesMade = true;
+            }
+            else if (libraryOptions?.EnableChapterImageExtraction != true)
+            {
+                // We have an image for the current chapter but the user has disabled chapter image extraction -> delete this chapter's image
+                chapter.ImagePath = null;
+                changesMade = true;
+            }
+        }
+
+        if (saveChapters && changesMade)
+        {
+            _chapterRepository.SaveChapters(video.Id, chapters);
+        }
+
+        DeleteDeadImages(currentImages, chapters);
+
+        return success;
+    }
+
+    /// <inheritdoc />
+    public void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters)
+    {
+        _chapterRepository.SaveChapters(video.Id, chapters);
+    }
+
+    /// <inheritdoc />
+    public ChapterInfo? GetChapter(Guid baseItemId, int index)
+    {
+        return _chapterRepository.GetChapter(baseItemId, index);
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId)
+    {
+        return _chapterRepository.GetChapters(baseItemId);
+    }
+
+    /// <inheritdoc />
+    public void DeleteChapterImages(Video video)
+    {
+        var path = _pathManager.GetChapterImageFolderPath(video);
+        try
+        {
+            if (Directory.Exists(path))
+            {
+                _logger.LogInformation("Removing chapter images for {Name} [{Id}]", video.Name, video.Id);
+                Directory.Delete(path, true);
+            }
+        }
+        catch (Exception ex)
+        {
+            _logger.LogWarning("Failed to remove chapter image folder for {Item}: {Exception}", video.Id, ex);
+        }
+
+        _chapterRepository.DeleteChapters(video.Id);
+    }
+
+    private IReadOnlyList<string> GetSavedChapterImages(Video video, IDirectoryService directoryService)
+    {
+        var path = _pathManager.GetChapterImageFolderPath(video);
+        if (!Directory.Exists(path))
+        {
+            return [];
+        }
+
+        try
+        {
+            return directoryService.GetFilePaths(path);
+        }
+        catch (IOException)
+        {
+            return [];
+        }
+    }
+
+    private void DeleteDeadImages(IEnumerable<string> images, IEnumerable<ChapterInfo> chapters)
+    {
+        var existingImages = chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i));
+        var deadImages = images
+            .Except(existingImages, StringComparer.OrdinalIgnoreCase)
+            .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
+            .ToList();
+
+        foreach (var image in deadImages)
+        {
+            _logger.LogDebug("Deleting dead chapter image {Path}", image);
+
+            try
+            {
+                _fileSystem.DeleteFile(image!);
+            }
+            catch (IOException ex)
+            {
+                _logger.LogError(ex, "Error deleting {Path}.", image);
+            }
+        }
+    }
+}

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

@@ -4,7 +4,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
+using Jellyfin.Database.Implementations.Entities;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -95,7 +95,7 @@ namespace Emby.Server.Implementations.Collections
 
 
             var libraryOptions = new LibraryOptions
             var libraryOptions = new LibraryOptions
             {
             {
-                PathInfos = new[] { new MediaPathInfo(path) },
+                PathInfos = [new MediaPathInfo(path)],
                 EnableRealtimeMonitor = false,
                 EnableRealtimeMonitor = false,
                 SaveLocalMetadata = true
                 SaveLocalMetadata = true
             };
             };
@@ -150,15 +150,15 @@ namespace Emby.Server.Implementations.Collections
 
 
             try
             try
             {
             {
-                Directory.CreateDirectory(path);
-
+                var info = Directory.CreateDirectory(path);
                 var collection = new BoxSet
                 var collection = new BoxSet
                 {
                 {
                     Name = name,
                     Name = name,
                     Path = path,
                     Path = path,
                     IsLocked = options.IsLocked,
                     IsLocked = options.IsLocked,
                     ProviderIds = options.ProviderIds,
                     ProviderIds = options.ProviderIds,
-                    DateCreated = DateTime.UtcNow
+                    DateCreated = info.CreationTimeUtc,
+                    DateModified = info.LastWriteTimeUtc
                 };
                 };
 
 
                 parentFolder.AddChild(collection);
                 parentFolder.AddChild(collection);
@@ -204,7 +204,7 @@ namespace Emby.Server.Implementations.Collections
         {
         {
             if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             if (_libraryManager.GetItemById(collectionId) is not BoxSet collection)
             {
             {
-                throw new ArgumentException("No collection exists with the supplied Id");
+                throw new ArgumentException("No collection exists with the supplied collectionId " + collectionId);
             }
             }
 
 
             List<BaseItem>? itemList = null;
             List<BaseItem>? itemList = null;
@@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Collections
 
 
                 if (item is null)
                 if (item is null)
                 {
                 {
-                    throw new ArgumentException("No item exists with the supplied Id");
+                    throw new ArgumentException("No item exists with the supplied Id " + id);
                 }
                 }
 
 
                 if (!currentLinkedChildrenIds.Contains(id))
                 if (!currentLinkedChildrenIds.Contains(id))

+ 81 - 51
Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs

@@ -1,84 +1,114 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System;
 using System;
+using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Jellyfin.Server.Implementations;
+using Jellyfin.Database.Implementations;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Data
+namespace Emby.Server.Implementations.Data;
+
+public class CleanDatabaseScheduledTask : ILibraryPostScanTask
 {
 {
-    public class CleanDatabaseScheduledTask : ILibraryPostScanTask
+    private readonly ILibraryManager _libraryManager;
+    private readonly ILogger<CleanDatabaseScheduledTask> _logger;
+    private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
+    private readonly IPathManager _pathManager;
+
+    public CleanDatabaseScheduledTask(
+        ILibraryManager libraryManager,
+        ILogger<CleanDatabaseScheduledTask> logger,
+        IDbContextFactory<JellyfinDbContext> dbProvider,
+        IPathManager pathManager)
     {
     {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<CleanDatabaseScheduledTask> _logger;
-        private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
-
-        public CleanDatabaseScheduledTask(
-            ILibraryManager libraryManager,
-            ILogger<CleanDatabaseScheduledTask> logger,
-            IDbContextFactory<JellyfinDbContext> dbProvider)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _dbProvider = dbProvider;
-        }
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _dbProvider = dbProvider;
+        _pathManager = pathManager;
+    }
 
 
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
-        }
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false);
+    }
 
 
-        private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+    private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress<double> progress)
+    {
+        var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
         {
         {
-            var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery
-            {
-                HasDeadParentId = true
-            });
+            HasDeadParentId = true
+        });
 
 
-            var numComplete = 0;
-            var numItems = itemIds.Count + 1;
+        var numComplete = 0;
+        var numItems = itemIds.Count + 1;
 
 
-            _logger.LogDebug("Cleaning {0} items with dead parent links", numItems);
+        _logger.LogDebug("Cleaning {Number} items with dead parents", numItems);
 
 
-            foreach (var itemId in itemIds)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
+        foreach (var itemId in itemIds)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
 
 
-                var item = _libraryManager.GetItemById(itemId);
+            var item = _libraryManager.GetItemById(itemId);
+            if (item is not null)
+            {
+                _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
 
 
-                if (item is not null)
+                foreach (var mediaSource in item.GetMediaSources(false))
                 {
                 {
-                    _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty);
+                    // Delete extracted data
+                    var mediaSourceItem = _libraryManager.GetItemById(mediaSource.Id);
+                    if (mediaSourceItem is null)
+                    {
+                        continue;
+                    }
 
 
-                    _libraryManager.DeleteItem(item, new DeleteOptions
+                    var extractedDataFolders = _pathManager.GetExtractedDataPaths(mediaSourceItem);
+                    foreach (var folder in extractedDataFolders)
                     {
                     {
-                        DeleteFileLocation = false
-                    });
+                        if (Directory.Exists(folder))
+                        {
+                            try
+                            {
+                                Directory.Delete(folder, true);
+                            }
+                            catch (Exception e)
+                            {
+                                _logger.LogWarning("Failed to remove {Folder}: {Exception}", folder, e.Message);
+                            }
+                        }
+                    }
                 }
                 }
 
 
-                numComplete++;
-                double percent = numComplete;
-                percent /= numItems;
-                progress.Report(percent * 100);
+                // Delete item
+                _libraryManager.DeleteItem(item, new DeleteOptions
+                {
+                    DeleteFileLocation = false
+                });
             }
             }
 
 
-            var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
-            await using (context.ConfigureAwait(false))
+            numComplete++;
+            double percent = numComplete;
+            percent /= numItems;
+            progress.Report(percent * 100);
+        }
+
+        var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+        await using (context.ConfigureAwait(false))
+        {
+            var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
+            await using (transaction.ConfigureAwait(false))
             {
             {
-                var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false);
-                await using (transaction.ConfigureAwait(false))
-                {
-                    await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
-                    await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
-                }
+                await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false);
+                await transaction.CommitAsync(cancellationToken).ConfigureAwait(false);
             }
             }
-
-            progress.Report(100);
         }
         }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 20 - 19
Emby.Server.Implementations/Dto/DtoService.cs

@@ -5,8 +5,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Common;
 using MediaBrowser.Common;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
@@ -17,7 +17,6 @@ using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Controller.Trickplay;
@@ -41,7 +40,6 @@ namespace Emby.Server.Implementations.Dto
         private readonly ILogger<DtoService> _logger;
         private readonly ILogger<DtoService> _logger;
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
         private readonly IUserDataManager _userDataRepository;
         private readonly IUserDataManager _userDataRepository;
-        private readonly IItemRepository _itemRepo;
 
 
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
@@ -52,13 +50,12 @@ namespace Emby.Server.Implementations.Dto
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 
 
         private readonly ITrickplayManager _trickplayManager;
         private readonly ITrickplayManager _trickplayManager;
-        private readonly IChapterRepository _chapterRepository;
+        private readonly IChapterManager _chapterManager;
 
 
         public DtoService(
         public DtoService(
             ILogger<DtoService> logger,
             ILogger<DtoService> logger,
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
             IUserDataManager userDataRepository,
             IUserDataManager userDataRepository,
-            IItemRepository itemRepo,
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
             IProviderManager providerManager,
             IProviderManager providerManager,
             IRecordingsManager recordingsManager,
             IRecordingsManager recordingsManager,
@@ -66,12 +63,11 @@ namespace Emby.Server.Implementations.Dto
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
             Lazy<ILiveTvManager> livetvManagerFactory,
             Lazy<ILiveTvManager> livetvManagerFactory,
             ITrickplayManager trickplayManager,
             ITrickplayManager trickplayManager,
-            IChapterRepository chapterRepository)
+            IChapterManager chapterManager)
         {
         {
             _logger = logger;
             _logger = logger;
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
             _userDataRepository = userDataRepository;
             _userDataRepository = userDataRepository;
-            _itemRepo = itemRepo;
             _imageProcessor = imageProcessor;
             _imageProcessor = imageProcessor;
             _providerManager = providerManager;
             _providerManager = providerManager;
             _recordingsManager = recordingsManager;
             _recordingsManager = recordingsManager;
@@ -79,7 +75,7 @@ namespace Emby.Server.Implementations.Dto
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _livetvManagerFactory = livetvManagerFactory;
             _livetvManagerFactory = livetvManagerFactory;
             _trickplayManager = trickplayManager;
             _trickplayManager = trickplayManager;
-            _chapterRepository = chapterRepository;
+            _chapterManager = chapterManager;
         }
         }
 
 
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -99,11 +95,11 @@ namespace Emby.Server.Implementations.Dto
 
 
                 if (item is LiveTvChannel tvChannel)
                 if (item is LiveTvChannel tvChannel)
                 {
                 {
-                    (channelTuples ??= new()).Add((dto, tvChannel));
+                    (channelTuples ??= []).Add((dto, tvChannel));
                 }
                 }
                 else if (item is LiveTvProgram)
                 else if (item is LiveTvProgram)
                 {
                 {
-                    (programTuples ??= new()).Add((item, dto));
+                    (programTuples ??= []).Add((item, dto));
                 }
                 }
 
 
                 if (item is IItemByName byName)
                 if (item is IItemByName byName)
@@ -590,12 +586,12 @@ namespace Emby.Server.Implementations.Dto
                     if (dto.ImageBlurHashes is not null)
                     if (dto.ImageBlurHashes is not null)
                     {
                     {
                         // Only add BlurHash for the person's image.
                         // Only add BlurHash for the person's image.
-                        baseItemPerson.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+                        baseItemPerson.ImageBlurHashes = [];
                         foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
                         foreach (var (imageType, blurHash) in dto.ImageBlurHashes)
                         {
                         {
                             if (blurHash is not null)
                             if (blurHash is not null)
                             {
                             {
-                                baseItemPerson.ImageBlurHashes[imageType] = new Dictionary<string, string>();
+                                baseItemPerson.ImageBlurHashes[imageType] = [];
                                 foreach (var (imageId, blurHashValue) in blurHash)
                                 foreach (var (imageId, blurHashValue) in blurHash)
                                 {
                                 {
                                     if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
                                     if (string.Equals(baseItemPerson.PrimaryImageTag, imageId, StringComparison.OrdinalIgnoreCase))
@@ -674,11 +670,11 @@ namespace Emby.Server.Implementations.Dto
 
 
             if (!string.IsNullOrEmpty(image.BlurHash))
             if (!string.IsNullOrEmpty(image.BlurHash))
             {
             {
-                dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+                dto.ImageBlurHashes ??= [];
 
 
                 if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
                 if (!dto.ImageBlurHashes.TryGetValue(image.Type, out var value))
                 {
                 {
-                    value = new Dictionary<string, string>();
+                    value = [];
                     dto.ImageBlurHashes[image.Type] = value;
                     dto.ImageBlurHashes[image.Type] = value;
                 }
                 }
 
 
@@ -709,7 +705,7 @@ namespace Emby.Server.Implementations.Dto
 
 
             if (hashes.Count > 0)
             if (hashes.Count > 0)
             {
             {
-                dto.ImageBlurHashes ??= new Dictionary<ImageType, Dictionary<string, string>>();
+                dto.ImageBlurHashes ??= [];
 
 
                 dto.ImageBlurHashes[imageType] = hashes;
                 dto.ImageBlurHashes[imageType] = hashes;
             }
             }
@@ -756,7 +752,7 @@ namespace Emby.Server.Implementations.Dto
                 dto.AspectRatio = hasAspectRatio.AspectRatio;
                 dto.AspectRatio = hasAspectRatio.AspectRatio;
             }
             }
 
 
-            dto.ImageBlurHashes = new Dictionary<ImageType, Dictionary<string, string>>();
+            dto.ImageBlurHashes = [];
 
 
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             var backdropLimit = options.GetImageLimit(ImageType.Backdrop);
             if (backdropLimit > 0)
             if (backdropLimit > 0)
@@ -772,7 +768,7 @@ namespace Emby.Server.Implementations.Dto
 
 
             if (options.EnableImages)
             if (options.EnableImages)
             {
             {
-                dto.ImageTags = new Dictionary<ImageType, string>();
+                dto.ImageTags = [];
 
 
                 // Prevent implicitly captured closure
                 // Prevent implicitly captured closure
                 var currentItem = item;
                 var currentItem = item;
@@ -1064,12 +1060,17 @@ namespace Emby.Server.Implementations.Dto
 
 
                 if (options.ContainsField(ItemFields.Chapters))
                 if (options.ContainsField(ItemFields.Chapters))
                 {
                 {
-                    dto.Chapters = _chapterRepository.GetChapters(item.Id).ToList();
+                    dto.Chapters = _chapterManager.GetChapters(item.Id).ToList();
                 }
                 }
 
 
                 if (options.ContainsField(ItemFields.Trickplay))
                 if (options.ContainsField(ItemFields.Trickplay))
                 {
                 {
-                    dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+                    var trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+                    dto.Trickplay = trickplay.ToDictionary(
+                        mediaStream => mediaStream.Key,
+                        mediaStream => mediaStream.Value.ToDictionary(
+                            width => width.Key,
+                            width => new TrickplayInfoDto(width.Value)));
                 }
                 }
 
 
                 dto.ExtraType = video.ExtraType;
                 dto.ExtraType = video.ExtraType;

+ 7 - 1
Emby.Server.Implementations/Emby.Server.Implementations.csproj

@@ -18,9 +18,11 @@
     <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
     <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" />
     <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
     <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" />
     <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
     <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" />
+    <ProjectReference Include="..\src\Jellyfin.Database\Jellyfin.Database.Implementations\Jellyfin.Database.Implementations.csproj" />
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
+    <PackageReference Include="BitFaster.Caching" />
     <PackageReference Include="DiscUtils.Udf" />
     <PackageReference Include="DiscUtils.Udf" />
     <PackageReference Include="Microsoft.Data.Sqlite" />
     <PackageReference Include="Microsoft.Data.Sqlite" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
@@ -62,10 +64,14 @@
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
+  <ItemGroup>
+    <PackageReference Include="Ignore" />
+  </ItemGroup>
+
   <ItemGroup>
   <ItemGroup>
     <EmbeddedResource Include="Localization\iso6392.txt" />
     <EmbeddedResource Include="Localization\iso6392.txt" />
     <EmbeddedResource Include="Localization\countries.json" />
     <EmbeddedResource Include="Localization\countries.json" />
     <EmbeddedResource Include="Localization\Core\*.json" />
     <EmbeddedResource Include="Localization\Core\*.json" />
-    <EmbeddedResource Include="Localization\Ratings\*.csv" />
+    <EmbeddedResource Include="Localization\Ratings\*.json" />
   </ItemGroup>
   </ItemGroup>
 </Project>
 </Project>

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

@@ -5,8 +5,8 @@ using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
+using Jellyfin.Database.Implementations.Entities;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;

+ 2 - 1
Emby.Server.Implementations/HttpServer/Security/AuthService.cs

@@ -1,7 +1,8 @@
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Threading.Tasks;
 using System.Threading.Tasks;
-using Jellyfin.Data.Enums;
+using Jellyfin.Data;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http;
 
 

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

@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.HttpServer
             RemoteEndPoint = remoteEndPoint;
             RemoteEndPoint = remoteEndPoint;
 
 
             _jsonOptions = JsonDefaults.Options;
             _jsonOptions = JsonDefaults.Options;
-            LastActivityDate = DateTime.Now;
+            LastActivityDate = DateTime.UtcNow;
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />

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

@@ -130,7 +130,7 @@ namespace Emby.Server.Implementations.IO
         private void ProcessPathChanges(List<string> paths)
         private void ProcessPathChanges(List<string> paths)
         {
         {
             IEnumerable<BaseItem> itemsToRefresh = paths
             IEnumerable<BaseItem> itemsToRefresh = paths
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Distinct()
                 .Select(GetAffectedBaseItem)
                 .Select(GetAffectedBaseItem)
                 .Where(item => item is not null)
                 .Where(item => item is not null)
                 .DistinctBy(x => x!.Id)!;  // Removed null values in the previous .Where()
                 .DistinctBy(x => x!.Id)!;  // Removed null values in the previous .Where()

+ 1 - 1
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -121,7 +121,7 @@ namespace Emby.Server.Implementations.IO
                 .Where(IsLibraryMonitorEnabled)
                 .Where(IsLibraryMonitorEnabled)
                 .OfType<Folder>()
                 .OfType<Folder>()
                 .SelectMany(f => f.PhysicalLocations)
                 .SelectMany(f => f.PhysicalLocations)
-                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .Distinct()
                 .Order();
                 .Order();
 
 
             foreach (var path in paths)
             foreach (var path in paths)

+ 24 - 9
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -160,12 +160,13 @@ namespace Emby.Server.Implementations.IO
             {
             {
                 // Cross device move requires a copy
                 // Cross device move requires a copy
                 Directory.CreateDirectory(destination);
                 Directory.CreateDirectory(destination);
-                foreach (string file in Directory.GetFiles(source))
+                var sourceDir = new DirectoryInfo(source);
+                foreach (var file in sourceDir.EnumerateFiles())
                 {
                 {
-                    File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), true);
+                    file.CopyTo(Path.Combine(destination, file.Name), true);
                 }
                 }
 
 
-                Directory.Delete(source, true);
+                sourceDir.Delete(true);
             }
             }
         }
         }
 
 
@@ -541,8 +542,8 @@ namespace Emby.Server.Implementations.IO
             return DriveInfo.GetDrives()
             return DriveInfo.GetDrives()
                 .Where(
                 .Where(
                     d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
                     d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
-                        && d.IsReady
-                        && d.TotalSize != 0)
+                         && d.IsReady
+                         && d.TotalSize != 0)
                 .Select(d => new FileSystemMetadata
                 .Select(d => new FileSystemMetadata
                 {
                 {
                     Name = d.Name,
                     Name = d.Name,
@@ -560,11 +561,23 @@ namespace Emby.Server.Implementations.IO
         /// <inheritdoc />
         /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
         {
         {
-            return GetFiles(path, null, false, recursive);
+            return GetFiles(path, "*", recursive);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
+        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, bool recursive = false)
+        {
+            return GetFiles(path, searchPattern, null, false, recursive);
+        }
+
+        /// <inheritdoc />
+        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive)
+        {
+            return GetFiles(path, "*", extensions, enableCaseSensitiveExtensions, recursive);
+        }
+
+        /// <inheritdoc />
+        public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, string searchPattern, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
         {
             var enumerationOptions = GetEnumerationOptions(recursive);
             var enumerationOptions = GetEnumerationOptions(recursive);
 
 
@@ -572,10 +585,12 @@ namespace Emby.Server.Implementations.IO
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
             if ((enableCaseSensitiveExtensions || _isEnvironmentCaseInsensitive) && extensions is not null && extensions.Count == 1)
             {
             {
-                return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], enumerationOptions));
+                searchPattern = searchPattern.EndsWith(extensions[0], StringComparison.Ordinal) ? searchPattern : searchPattern + extensions[0];
+
+                return ToMetadata(new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions));
             }
             }
 
 
-            var files = new DirectoryInfo(path).EnumerateFiles("*", enumerationOptions);
+            var files = new DirectoryInfo(path).EnumerateFiles(searchPattern, enumerationOptions);
 
 
             if (extensions is not null && extensions.Count > 0)
             if (extensions is not null && extensions.Count > 0)
             {
             {

+ 9 - 5
Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs

@@ -43,13 +43,11 @@ namespace Emby.Server.Implementations.Images
         protected IImageProcessor ImageProcessor { get; set; }
         protected IImageProcessor ImageProcessor { get; set; }
 
 
         protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
         protected virtual IReadOnlyCollection<ImageType> SupportedImages { get; }
-            = new ImageType[] { ImageType.Primary };
+            = [ImageType.Primary];
 
 
         /// <inheritdoc />
         /// <inheritdoc />
         public string Name => "Dynamic Image Provider";
         public string Name => "Dynamic Image Provider";
 
 
-        protected virtual int MaxImageAgeDays => 7;
-
         public int Order => 0;
         public int Order => 0;
 
 
         protected virtual bool Supports(BaseItem item) => true;
         protected virtual bool Supports(BaseItem item) => true;
@@ -292,8 +290,14 @@ namespace Emby.Server.Implementations.Images
 
 
         protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
         protected virtual bool HasChangedByDate(BaseItem item, ItemImageInfo image)
         {
         {
-            var age = DateTime.UtcNow - image.DateModified;
-            return age.TotalDays > MaxImageAgeDays;
+            var path = image.Path;
+            if (!string.IsNullOrEmpty(path))
+            {
+                var modificationDate = FileSystem.GetLastWriteTimeUtc(path);
+                return image.DateModified != modificationDate;
+            }
+
+            return false;
         }
         }
 
 
         protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)
         protected string CreateSingleImage(IEnumerable<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType)

+ 1 - 0
Emby.Server.Implementations/Images/BaseFolderImageProvider.cs

@@ -2,6 +2,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -6,6 +6,7 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
Emby.Server.Implementations/Images/GenreImageProvider.cs

@@ -2,6 +2,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

+ 1 - 0
Emby.Server.Implementations/Images/MusicGenreImageProvider.cs

@@ -4,6 +4,7 @@
 
 
 using System.Collections.Generic;
 using System.Collections.Generic;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;

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

@@ -54,7 +54,7 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 if (parent is not null)
                 if (parent is not null)
                 {
                 {
-                    // Ignore extras folders but allow it at the collection level
+                    // Ignore extras for unsupported types
                     if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
                     if (_namingOptions.AllExtrasTypesFolderNames.ContainsKey(filename)
                         && parent is not AggregateFolder
                         && parent is not AggregateFolder
                         && parent is not UserRootFolder)
                         && parent is not UserRootFolder)
@@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.Library
             {
             {
                 if (parent is not null)
                 if (parent is not null)
                 {
                 {
-                    // Don't resolve these into audio files
+                    // Don't resolve theme songs
                     if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
                     if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
                         && AudioFileParser.IsAudioFile(filename, _namingOptions))
                         && AudioFileParser.IsAudioFile(filename, _namingOptions))
                     {
                     {

+ 94 - 0
Emby.Server.Implementations/Library/DotIgnoreIgnoreRule.cs

@@ -0,0 +1,94 @@
+using System;
+using System.IO;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Model.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Resolver rule class for ignoring files via .ignore.
+/// </summary>
+public class DotIgnoreIgnoreRule : IResolverIgnoreRule
+{
+    private static FileInfo? FindIgnoreFile(DirectoryInfo directory)
+    {
+        var ignoreFile = new FileInfo(Path.Join(directory.FullName, ".ignore"));
+        if (ignoreFile.Exists)
+        {
+            return ignoreFile;
+        }
+
+        var parentDir = directory.Parent;
+        if (parentDir is null)
+        {
+            return null;
+        }
+
+        return FindIgnoreFile(parentDir);
+    }
+
+    /// <inheritdoc />
+    public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem? parent)
+    {
+        return IsIgnored(fileInfo, parent);
+    }
+
+    /// <summary>
+    /// Checks whether or not the file is ignored.
+    /// </summary>
+    /// <param name="fileInfo">The file information.</param>
+    /// <param name="parent">The parent BaseItem.</param>
+    /// <returns>True if the file should be ignored.</returns>
+    public static bool IsIgnored(FileSystemMetadata fileInfo, BaseItem? parent)
+    {
+        if (fileInfo.IsDirectory)
+        {
+            var dirIgnoreFile = FindIgnoreFile(new DirectoryInfo(fileInfo.FullName));
+            if (dirIgnoreFile is null)
+            {
+                return false;
+            }
+
+            // ignore the directory only if the .ignore file is empty
+            // evaluate individual files otherwise
+            return string.IsNullOrWhiteSpace(GetFileContent(dirIgnoreFile));
+        }
+
+        var parentDirPath = Path.GetDirectoryName(fileInfo.FullName);
+        if (string.IsNullOrEmpty(parentDirPath))
+        {
+            return false;
+        }
+
+        var folder = new DirectoryInfo(parentDirPath);
+        var ignoreFile = FindIgnoreFile(folder);
+        if (ignoreFile is null)
+        {
+            return false;
+        }
+
+        string ignoreFileString = GetFileContent(ignoreFile);
+
+        if (string.IsNullOrWhiteSpace(ignoreFileString))
+        {
+            // Ignore directory if we just have the file
+            return true;
+        }
+
+        // If file has content, base ignoring off the content .gitignore-style rules
+        var ignoreRules = ignoreFileString.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+        var ignore = new Ignore.Ignore();
+        ignore.Add(ignoreRules);
+
+        return ignore.IsIgnored(fileInfo.FullName);
+    }
+
+    private static string GetFileContent(FileInfo dirIgnoreFile)
+    {
+        using (var reader = dirIgnoreFile.OpenText())
+        {
+            return reader.ReadToEnd();
+        }
+    }
+}

+ 71 - 0
Emby.Server.Implementations/Library/ExternalDataManager.cs

@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.MediaSegments;
+using MediaBrowser.Controller.Trickplay;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IExternalDataManager implementation.
+/// </summary>
+public class ExternalDataManager : IExternalDataManager
+{
+    private readonly IKeyframeManager _keyframeManager;
+    private readonly IMediaSegmentManager _mediaSegmentManager;
+    private readonly IPathManager _pathManager;
+    private readonly ITrickplayManager _trickplayManager;
+    private readonly ILogger<ExternalDataManager> _logger;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ExternalDataManager"/> class.
+    /// </summary>
+    /// <param name="keyframeManager">The keyframe manager.</param>
+    /// <param name="mediaSegmentManager">The media segment manager.</param>
+    /// <param name="pathManager">The path manager.</param>
+    /// <param name="trickplayManager">The trickplay manager.</param>
+    /// <param name="logger">The logger.</param>
+    public ExternalDataManager(
+        IKeyframeManager keyframeManager,
+        IMediaSegmentManager mediaSegmentManager,
+        IPathManager pathManager,
+        ITrickplayManager trickplayManager,
+        ILogger<ExternalDataManager> logger)
+    {
+        _keyframeManager = keyframeManager;
+        _mediaSegmentManager = mediaSegmentManager;
+        _pathManager = pathManager;
+        _trickplayManager = trickplayManager;
+        _logger = logger;
+    }
+
+    /// <inheritdoc/>
+    public async Task DeleteExternalItemDataAsync(BaseItem item, CancellationToken cancellationToken)
+    {
+        var validPaths = _pathManager.GetExtractedDataPaths(item).Where(Directory.Exists).ToList();
+        var itemId = item.Id;
+        if (validPaths.Count > 0)
+        {
+            foreach (var path in validPaths)
+            {
+                try
+                {
+                    Directory.Delete(path, true);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogWarning("Unable to prune external item data at {Path}: {Exception}", path, ex);
+                }
+            }
+        }
+
+        await _keyframeManager.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+        await _mediaSegmentManager.DeleteSegmentsAsync(itemId, cancellationToken).ConfigureAwait(false);
+        await _trickplayManager.DeleteTrickplayDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+    }
+}

+ 0 - 1
Emby.Server.Implementations/Library/IgnorePatterns.cs

@@ -1,5 +1,4 @@
 using System;
 using System;
-using System.Linq;
 using DotNet.Globbing;
 using DotNet.Globbing;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library

+ 44 - 0
Emby.Server.Implementations/Library/KeyframeManager.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Jellyfin.MediaEncoding.Keyframes;
+using MediaBrowser.Controller.IO;
+using MediaBrowser.Controller.Persistence;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// Manager for Keyframe data.
+/// </summary>
+public class KeyframeManager : IKeyframeManager
+{
+    private readonly IKeyframeRepository _repository;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="KeyframeManager"/> class.
+    /// </summary>
+    /// <param name="repository">The keyframe repository.</param>
+    public KeyframeManager(IKeyframeRepository repository)
+    {
+        _repository = repository;
+    }
+
+    /// <inheritdoc />
+    public IReadOnlyList<KeyframeData> GetKeyframeData(Guid itemId)
+    {
+        return _repository.GetKeyframeData(itemId);
+    }
+
+    /// <inheritdoc />
+    public async Task SaveKeyframeDataAsync(Guid itemId, KeyframeData data, CancellationToken cancellationToken)
+    {
+        await _repository.SaveKeyframeDataAsync(itemId, data, cancellationToken).ConfigureAwait(false);
+    }
+
+    /// <inheritdoc />
+    public async Task DeleteKeyframeDataAsync(Guid itemId, CancellationToken cancellationToken)
+    {
+        await _repository.DeleteKeyframeDataAsync(itemId, cancellationToken).ConfigureAwait(false);
+    }
+}

File diff suppressed because it is too large
+ 239 - 165
Emby.Server.Implementations/Library/LibraryManager.cs


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

@@ -13,8 +13,10 @@ using System.Text.Json;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using AsyncKeyedLock;
 using AsyncKeyedLock;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
@@ -425,6 +427,7 @@ namespace Emby.Server.Implementations.Library
                 if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
                 if (source.MediaStreams.Any(i => i.Type == MediaStreamType.Audio && i.Index == index))
                 {
                 {
                     source.DefaultAudioStreamIndex = index;
                     source.DefaultAudioStreamIndex = index;
+                    source.DefaultAudioIndexSource = AudioIndexSource.User;
                     return;
                     return;
                 }
                 }
             }
             }
@@ -432,6 +435,15 @@ namespace Emby.Server.Implementations.Library
             var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
             var preferredAudio = NormalizeLanguage(user.AudioLanguagePreference);
 
 
             source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
             source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
+            if (user.PlayDefaultAudioTrack)
+            {
+                source.DefaultAudioIndexSource |= AudioIndexSource.Default;
+            }
+
+            if (preferredAudio.Count > 0)
+            {
+                source.DefaultAudioIndexSource |= AudioIndexSource.Language;
+            }
         }
         }
 
 
         public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
         public void SetDefaultAudioAndSubtitleStreamIndices(BaseItem item, MediaSourceInfo source, User user)
@@ -669,17 +681,17 @@ namespace Emby.Server.Implementations.Library
 
 
                 mediaInfo = await _mediaEncoder.GetMediaInfo(
                 mediaInfo = await _mediaEncoder.GetMediaInfo(
                     new MediaInfoRequest
                     new MediaInfoRequest
-                {
-                    MediaSource = mediaSource,
-                    MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
-                    ExtractChapters = false
-                },
+                    {
+                        MediaSource = mediaSource,
+                        MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
+                        ExtractChapters = false
+                    },
                     cancellationToken).ConfigureAwait(false);
                     cancellationToken).ConfigureAwait(false);
 
 
                 if (cacheFilePath is not null)
                 if (cacheFilePath is not null)
                 {
                 {
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
                     Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
-                    FileStream createStream = File.Create(cacheFilePath);
+                    FileStream createStream = AsyncFile.Create(cacheFilePath);
                     await using (createStream.ConfigureAwait(false))
                     await using (createStream.ConfigureAwait(false))
                     {
                     {
                         await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
                         await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
@@ -782,9 +794,13 @@ namespace Emby.Server.Implementations.Library
         {
         {
             ArgumentException.ThrowIfNullOrEmpty(id);
             ArgumentException.ThrowIfNullOrEmpty(id);
 
 
-            // TODO probably shouldn't throw here but it is kept for "backwards compatibility"
-            var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException();
-            return Task.FromResult(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
+            var info = GetLiveStreamInfo(id);
+            if (info is null)
+            {
+                return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(null, null));
+            }
+
+            return Task.FromResult<Tuple<MediaSourceInfo, IDirectStreamProvider>>(new Tuple<MediaSourceInfo, IDirectStreamProvider>(info.MediaSource, info as IDirectStreamProvider));
         }
         }
 
 
         public ILiveStream GetLiveStreamInfo(string id)
         public ILiveStream GetLiveStreamInfo(string id)

+ 61 - 27
Emby.Server.Implementations/Library/MediaStreamSelector.cs

@@ -3,7 +3,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
@@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library
                 return null;
                 return null;
             }
             }
 
 
+            // Sort in the following order: Default > No tag > Forced
             var sortedStreams = streams
             var sortedStreams = streams
                 .Where(i => i.Type == MediaStreamType.Subtitle)
                 .Where(i => i.Type == MediaStreamType.Subtitle)
                 .OrderByDescending(x => x.IsExternal)
                 .OrderByDescending(x => x.IsExternal)
-                .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
-                .ThenByDescending(x => x.IsForced)
                 .ThenByDescending(x => x.IsDefault)
                 .ThenByDescending(x => x.IsDefault)
-                .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase))
+                .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+                .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages))
+                .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language))
+                .ThenByDescending(x => x.IsForced)
                 .ToList();
                 .ToList();
 
 
             MediaStream? stream = null;
             MediaStream? stream = null;
+
             if (mode == SubtitlePlaybackMode.Default)
             if (mode == SubtitlePlaybackMode.Default)
             {
             {
-                // Load subtitles according to external, forced and default flags.
-                stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+                // Load subtitles according to external, default and forced flags.
+                stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced);
             }
             }
             else if (mode == SubtitlePlaybackMode.Smart)
             else if (mode == SubtitlePlaybackMode.Smart)
             {
             {
                 // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
                 // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages.
-                // If no subtitles of preferred language available, use default behaviour.
+                // If no subtitles of preferred language available, use none.
+                // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced.
                 if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
-                        sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+                    stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages));
                 }
                 }
                 else
                 else
                 {
                 {
-                    // Respect forced flag.
-                    stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+                    stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
                 }
                 }
             }
             }
             else if (mode == SubtitlePlaybackMode.Always)
             else if (mode == SubtitlePlaybackMode.Always)
             {
             {
-                // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour.
-                stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ??
-                    sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault);
+                // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour.
+                stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ??
+                    BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
             }
             }
             else if (mode == SubtitlePlaybackMode.OnlyForced)
             else if (mode == SubtitlePlaybackMode.OnlyForced)
             {
             {
-                // Only load subtitles that are flagged forced.
-                stream = sortedStreams.FirstOrDefault(x => x.IsForced);
+                // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+                stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault();
             }
             }
 
 
             return stream?.Index;
             return stream?.Index;
@@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library
             if (mode == SubtitlePlaybackMode.Default)
             if (mode == SubtitlePlaybackMode.Default)
             {
             {
                 // Prefer embedded metadata over smart logic
                 // Prefer embedded metadata over smart logic
-                filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault)
+                // Load subtitles according to external, default, and forced flags.
+                filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced)
                     .ToList();
                     .ToList();
             }
             }
             else if (mode == SubtitlePlaybackMode.Smart)
             else if (mode == SubtitlePlaybackMode.Smart)
             {
             {
                 // Prefer smart logic over embedded metadata
                 // Prefer smart logic over embedded metadata
+                // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior.
                 if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
                 {
                 {
-                    filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase))
+                    filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
                         .ToList();
                         .ToList();
                 }
                 }
+                else
+                {
+                    filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
+                }
             }
             }
             else if (mode == SubtitlePlaybackMode.Always)
             else if (mode == SubtitlePlaybackMode.Always)
             {
             {
-                // Always load the most suitable full subtitles
-                filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList();
+                // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior.
+                filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages))
+                    .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages);
             }
             }
             else if (mode == SubtitlePlaybackMode.OnlyForced)
             else if (mode == SubtitlePlaybackMode.OnlyForced)
             {
             {
-                // Always load the most suitable full subtitles
-                filteredStreams = sortedStreams.Where(s => s.IsForced).ToList();
+                // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language
+                filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages);
             }
             }
 
 
-            // Load forced subs if we have found no suitable full subtitles
-            var iterStreams = filteredStreams is null || filteredStreams.Count == 0
-                ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase))
-                : filteredStreams;
+            // If filteredStreams is null, initialize it as an empty list to avoid null reference errors
+            filteredStreams ??= new List<MediaStream>();
 
 
-            foreach (var stream in iterStreams)
+            foreach (var stream in filteredStreams)
             {
             {
                 stream.Score = GetStreamScore(stream, preferredLanguages);
                 stream.Score = GetStreamScore(stream, preferredLanguages);
             }
             }
         }
         }
 
 
+        private static bool MatchesPreferredLanguage(string language, IReadOnlyList<string> preferredLanguages)
+        {
+            // If preferredLanguages is empty, treat it as "any language" (wildcard)
+            return preferredLanguages.Count == 0 ||
+                preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase);
+        }
+
+        private static bool IsLanguageUndefined(string language)
+        {
+            // Check for null, empty, or known placeholders
+            return string.IsNullOrEmpty(language) ||
+                language.Equals("und", StringComparison.OrdinalIgnoreCase) ||
+                language.Equals("unknown", StringComparison.OrdinalIgnoreCase) ||
+                language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) ||
+                language.Equals("mul", StringComparison.OrdinalIgnoreCase) ||
+                language.Equals("zxx", StringComparison.OrdinalIgnoreCase);
+        }
+
+        private static List<MediaStream> BehaviorOnlyForced(IEnumerable<MediaStream> sortedStreams, IReadOnlyList<string> preferredLanguages)
+        {
+            return sortedStreams
+                .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language)))
+                .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages))
+                .ThenByDescending(s => IsLanguageUndefined(s.Language))
+                .ToList();
+        }
+
         internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
         internal static int GetStreamScore(MediaStream stream, IReadOnlyList<string> languagePreferences)
         {
         {
             var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));
             var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase));

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

@@ -4,8 +4,9 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Collections.Immutable;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;

+ 101 - 0
Emby.Server.Implementations/Library/PathManager.cs

@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.IO;
+
+namespace Emby.Server.Implementations.Library;
+
+/// <summary>
+/// IPathManager implementation.
+/// </summary>
+public class PathManager : IPathManager
+{
+    private readonly IServerConfigurationManager _config;
+    private readonly IApplicationPaths _appPaths;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PathManager"/> class.
+    /// </summary>
+    /// <param name="config">The server configuration manager.</param>
+    /// <param name="appPaths">The application paths.</param>
+    public PathManager(
+        IServerConfigurationManager config,
+        IApplicationPaths appPaths)
+    {
+        _config = config;
+        _appPaths = appPaths;
+    }
+
+    private string SubtitleCachePath => Path.Combine(_appPaths.DataPath, "subtitles");
+
+    private string AttachmentCachePath => Path.Combine(_appPaths.DataPath, "attachments");
+
+    /// <inheritdoc />
+    public string GetAttachmentPath(string mediaSourceId, string fileName)
+    {
+        return Path.Combine(GetAttachmentFolderPath(mediaSourceId), fileName);
+    }
+
+    /// <inheritdoc />
+    public string GetAttachmentFolderPath(string mediaSourceId)
+    {
+        var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+        return Path.Join(AttachmentCachePath, id[..2], id);
+    }
+
+    /// <inheritdoc />
+    public string GetSubtitleFolderPath(string mediaSourceId)
+    {
+        var id = Guid.Parse(mediaSourceId).ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+        return Path.Join(SubtitleCachePath, id[..2], id);
+    }
+
+    /// <inheritdoc />
+    public string GetSubtitlePath(string mediaSourceId, int streamIndex, string extension)
+    {
+        return Path.Combine(GetSubtitleFolderPath(mediaSourceId), streamIndex.ToString(CultureInfo.InvariantCulture) + extension);
+    }
+
+    /// <inheritdoc />
+    public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false)
+    {
+        var id = item.Id.ToString("D", CultureInfo.InvariantCulture).AsSpan();
+
+        return saveWithMedia
+            ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(Path.GetFileName(item.Path), ".trickplay"))
+            : Path.Join(_config.ApplicationPaths.TrickplayPath, id[..2], id);
+    }
+
+    /// <inheritdoc/>
+    public string GetChapterImageFolderPath(BaseItem item)
+    {
+        return Path.Combine(item.GetInternalMetadataPath(), "chapters");
+    }
+
+    /// <inheritdoc/>
+    public string GetChapterImagePath(BaseItem item, long chapterPositionTicks)
+    {
+        var filename = item.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + "_" + chapterPositionTicks.ToString(CultureInfo.InvariantCulture) + ".jpg";
+
+        return Path.Combine(GetChapterImageFolderPath(item), filename);
+    }
+
+    /// <inheritdoc/>
+    public IReadOnlyList<string> GetExtractedDataPaths(BaseItem item)
+    {
+        var mediaSourceId = item.Id.ToString("N", CultureInfo.InvariantCulture);
+        return [
+            GetAttachmentFolderPath(mediaSourceId),
+            GetSubtitleFolderPath(mediaSourceId),
+            GetTrickplayDirectory(item, false),
+            GetTrickplayDirectory(item, true),
+            GetChapterImageFolderPath(item)
+        ];
+    }
+}

+ 16 - 6
Emby.Server.Implementations/Library/ResolverHelper.cs

@@ -136,23 +136,33 @@ namespace Emby.Server.Implementations.Library
 
 
             if (config.UseFileCreationTimeForDateAdded)
             if (config.UseFileCreationTimeForDateAdded)
             {
             {
-                // directoryService.getFile may return null
-                if (info is not null)
+                var fileCreationDate = info?.CreationTimeUtc;
+                if (fileCreationDate is not null)
                 {
                 {
-                    var dateCreated = info.CreationTimeUtc;
-
-                    if (dateCreated.Equals(DateTime.MinValue))
+                    var dateCreated = fileCreationDate;
+                    if (dateCreated == DateTime.MinValue)
                     {
                     {
                         dateCreated = DateTime.UtcNow;
                         dateCreated = DateTime.UtcNow;
                     }
                     }
 
 
-                    item.DateCreated = dateCreated;
+                    item.DateCreated = dateCreated.Value;
                 }
                 }
             }
             }
             else
             else
             {
             {
                 item.DateCreated = DateTime.UtcNow;
                 item.DateCreated = DateTime.UtcNow;
             }
             }
+
+            if (info is not null && !info.IsDirectory)
+            {
+                item.Size = info.Length;
+            }
+
+            var fileModificationDate = info?.LastWriteTimeUtc;
+            if (fileModificationDate.HasValue)
+            {
+                item.DateModified = fileModificationDate.Value;
+            }
         }
         }
     }
     }
 }
 }

+ 2 - 2
Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs

@@ -54,9 +54,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
             _ => _videoResolvers
             _ => _videoResolvers
         };
         };
 
 
-        public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType)
+        public bool TryGetExtraTypeForOwner(string path, VideoFileInfo ownerVideoFileInfo, [NotNullWhen(true)] out ExtraType? extraType, string? libraryRoot = "")
         {
         {
-            var extraResult = GetExtraInfo(path, _namingOptions);
+            var extraResult = GetExtraInfo(path, _namingOptions, libraryRoot);
             if (extraResult.ExtraType is null)
             if (extraResult.ExtraType is null)
             {
             {
                 extraType = null;
                 extraType = null;

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

@@ -270,11 +270,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             }
             }
 
 
             var videoInfos = files
             var videoInfos = files
-                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName))
+                .Select(i => VideoResolver.Resolve(i.FullName, i.IsDirectory, NamingOptions, parseName, parent.ContainingFolderPath))
                 .Where(f => f is not null)
                 .Where(f => f is not null)
                 .ToList();
                 .ToList();
 
 
-            var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName);
+            var resolverResult = VideoListResolver.Resolve(videoInfos, NamingOptions, supportMultiEditions, parseName, parent.ContainingFolderPath);
 
 
             var result = new MultiItemResolverResult
             var result = new MultiItemResolverResult
             {
             {
@@ -456,12 +456,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             {
             {
                 var videoPath = result.Items[0].Path;
                 var videoPath = result.Items[0].Path;
                 var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
                 var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
+                var hasOtherSubfolders = multiDiscFolders.Count > 0;
 
 
-                if (!hasPhotos)
+                if (!hasPhotos && !hasOtherSubfolders)
                 {
                 {
                     var movie = (T)result.Items[0];
                     var movie = (T)result.Items[0];
                     movie.IsInMixedFolder = false;
                     movie.IsInMixedFolder = false;
-                    movie.Name = Path.GetFileName(movie.ContainingFolderPath);
+                    if (collectionType == CollectionType.movies || collectionType is null)
+                    {
+                        movie.Name = Path.GetFileName(movie.ContainingFolderPath);
+                    }
+
                     return movie;
                     return movie;
                 }
                 }
             }
             }

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

@@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
 
 
                 var path = args.Path;
                 var path = args.Path;
 
 
-                var seasonParserResult = SeasonPathParser.Parse(path, true, true);
+                var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true);
 
 
                 var season = new Season
                 var season = new Season
                 {
                 {

+ 4 - 3
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
             {
             {
                 if (child.IsDirectory)
                 if (child.IsDirectory)
                 {
                 {
-                    if (IsSeasonFolder(child.FullName, isTvContentType))
+                    if (IsSeasonFolder(child.FullName, path, isTvContentType))
                     {
                     {
                         _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
                         _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
                         return true;
                         return true;
@@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// Determines whether [is season folder] [the specified path].
         /// Determines whether [is season folder] [the specified path].
         /// </summary>
         /// </summary>
         /// <param name="path">The path.</param>
         /// <param name="path">The path.</param>
+        /// <param name="parentPath">The parentpath.</param>
         /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
         /// <param name="isTvContentType">if set to <c>true</c> [is tv content type].</param>
         /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
         /// <returns><c>true</c> if [is season folder] [the specified path]; otherwise, <c>false</c>.</returns>
-        private static bool IsSeasonFolder(string path, bool isTvContentType)
+        private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType)
         {
         {
-            var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber;
+            var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber;
 
 
             return seasonNumber.HasValue;
             return seasonNumber.HasValue;
         }
         }

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

@@ -3,8 +3,9 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;

+ 7 - 7
Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs

@@ -4,13 +4,13 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Library;
 namespace Emby.Server.Implementations.Library;
@@ -77,15 +77,15 @@ public class SplashscreenPostScanTask : ILibraryPostScanTask
             CollapseBoxSetItems = false,
             CollapseBoxSetItems = false,
             Recursive = true,
             Recursive = true,
             DtoOptions = new DtoOptions(false),
             DtoOptions = new DtoOptions(false),
-            ImageTypes = new[] { imageType },
+            ImageTypes = [imageType],
             Limit = 30,
             Limit = 30,
             // TODO max parental rating configurable
             // TODO max parental rating configurable
-            MaxParentalRating = 10,
-            OrderBy = new[]
-            {
+            MaxParentalRating = new(10, null),
+            OrderBy =
+            [
                 (ItemSortBy.Random, SortOrder.Ascending)
                 (ItemSortBy.Random, SortOrder.Ascending)
-            },
-            IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series }
+            ],
+            IncludeItemTypes = [BaseItemKind.Movie, BaseItemKind.Series]
         });
         });
     }
     }
 }
 }

+ 8 - 10
Emby.Server.Implementations/Library/UserDataManager.cs

@@ -1,14 +1,13 @@
 #pragma warning disable RS0030 // Do not use banned APIs
 #pragma warning disable RS0030 // Do not use banned APIs
 
 
 using System;
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
-using Jellyfin.Data.Entities;
-using Jellyfin.Extensions;
-using Jellyfin.Server.Implementations;
+using BitFaster.Caching.Lru;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -26,11 +25,9 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     /// </summary>
     public class UserDataManager : IUserDataManager
     public class UserDataManager : IUserDataManager
     {
     {
-        private readonly ConcurrentDictionary<string, UserItemData> _userData =
-            new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase);
-
         private readonly IServerConfigurationManager _config;
         private readonly IServerConfigurationManager _config;
         private readonly IDbContextFactory<JellyfinDbContext> _repository;
         private readonly IDbContextFactory<JellyfinDbContext> _repository;
+        private readonly FastConcurrentLru<string, UserItemData> _cache;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="UserDataManager"/> class.
         /// Initializes a new instance of the <see cref="UserDataManager"/> class.
@@ -43,6 +40,7 @@ namespace Emby.Server.Implementations.Library
         {
         {
             _config = config;
             _config = config;
             _repository = repository;
             _repository = repository;
+            _cache = new FastConcurrentLru<string, UserItemData>(Environment.ProcessorCount, _config.Configuration.CacheSize, StringComparer.OrdinalIgnoreCase);
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
@@ -81,7 +79,7 @@ namespace Emby.Server.Implementations.Library
 
 
             var userId = user.InternalId;
             var userId = user.InternalId;
             var cacheKey = GetCacheKey(userId, item.Id);
             var cacheKey = GetCacheKey(userId, item.Id);
-            _userData.AddOrUpdate(cacheKey, userData, (_, _) => userData);
+            _cache.AddOrUpdate(cacheKey, userData);
 
 
             UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
             UserDataSaved?.Invoke(this, new UserDataSaveEventArgs
             {
             {
@@ -182,7 +180,7 @@ namespace Emby.Server.Implementations.Library
         {
         {
             var cacheKey = GetCacheKey(user.InternalId, itemId);
             var cacheKey = GetCacheKey(user.InternalId, itemId);
 
 
-            if (_userData.TryGetValue(cacheKey, out var data))
+            if (_cache.TryGet(cacheKey, out var data))
             {
             {
                 return data;
                 return data;
             }
             }
@@ -197,7 +195,7 @@ namespace Emby.Server.Implementations.Library
                 };
                 };
             }
             }
 
 
-            return _userData.GetOrAdd(cacheKey, data);
+            return _cache.GetOrAdd(cacheKey, _ => data);
         }
         }
 
 
         private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)
         private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List<string> keys)

+ 18 - 1
Emby.Server.Implementations/Library/UserViewManager.cs

@@ -6,8 +6,10 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
-using Jellyfin.Data.Entities;
+using Jellyfin.Data;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Database.Implementations.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -370,6 +372,21 @@ namespace Emby.Server.Implementations.Library
                 MediaTypes = mediaTypes
                 MediaTypes = mediaTypes
             };
             };
 
 
+            if (request.GroupItems)
+            {
+                if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.tvshows))
+                {
+                    query.Limit = limit;
+                    return _libraryManager.GetLatestItemList(query, parents, CollectionType.tvshows);
+                }
+
+                if (parents.OfType<ICollectionFolder>().All(i => i.CollectionType == CollectionType.music))
+                {
+                    query.Limit = limit;
+                    return _libraryManager.GetLatestItemList(query, parents, CollectionType.music);
+                }
+            }
+
             return _libraryManager.GetItemList(query, parents);
             return _libraryManager.GetItemList(query, parents);
         }
         }
     }
     }

+ 34 - 35
Emby.Server.Implementations/Library/Validators/ArtistsPostScanTask.cs

@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsPostScanTask.
+/// </summary>
+public class ArtistsPostScanTask : ILibraryPostScanTask
 {
 {
     /// <summary>
     /// <summary>
-    /// Class ArtistsPostScanTask.
+    /// The _library manager.
     /// </summary>
     /// </summary>
-    public class ArtistsPostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<ArtistsValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    private readonly ILibraryManager _libraryManager;
+    private readonly ILogger<ArtistsValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public ArtistsPostScanTask(
-            ILibraryManager libraryManager,
-            ILogger<ArtistsValidator> logger,
-            IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ArtistsPostScanTask" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public ArtistsPostScanTask(
+        ILibraryManager libraryManager,
+        ILogger<ArtistsValidator> logger,
+        IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
-        }
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        return new ArtistsValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
     }
     }
 }
 }

+ 78 - 79
Emby.Server.Implementations/Library/Validators/ArtistsValidator.cs

@@ -10,102 +10,101 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class ArtistsValidator.
+/// </summary>
+public class ArtistsValidator
 {
 {
     /// <summary>
     /// <summary>
-    /// Class ArtistsValidator.
+    /// The library manager.
     /// </summary>
     /// </summary>
-    public class ArtistsValidator
-    {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
+    private readonly ILibraryManager _libraryManager;
 
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<ArtistsValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    /// <summary>
+    /// The logger.
+    /// </summary>
+    private readonly ILogger<ArtistsValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ArtistsValidator" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public ArtistsValidator(ILibraryManager libraryManager, ILogger<ArtistsValidator> logger, IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            var names = _itemRepo.GetAllArtistNames();
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        var names = _itemRepo.GetAllArtistNames();
 
 
-            var numComplete = 0;
-            var count = names.Count;
+        var numComplete = 0;
+        var count = names.Count;
 
 
-            foreach (var name in names)
+        foreach (var name in names)
+        {
+            try
             {
             {
-                try
-                {
-                    var item = _libraryManager.GetArtist(name);
-
-                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException)
-                {
-                    // Don't clutter the log
-                    throw;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error refreshing {ArtistName}", name);
-                }
-
-                numComplete++;
-                double percent = numComplete;
-                percent /= count;
-                percent *= 100;
+                var item = _libraryManager.GetArtist(name);
 
 
-                progress.Report(percent);
+                await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
             }
             }
-
-            var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+            catch (OperationCanceledException)
             {
             {
-                IncludeItemTypes = new[] { BaseItemKind.MusicArtist },
-                IsDeadArtist = true,
-                IsLocked = false
-            }).Cast<MusicArtist>().ToList();
-
-            foreach (var item in deadEntities)
+                // Don't clutter the log
+                throw;
+            }
+            catch (Exception ex)
             {
             {
-                if (!item.IsAccessedByName)
-                {
-                    continue;
-                }
+                _logger.LogError(ex, "Error refreshing {ArtistName}", name);
+            }
 
 
-                _logger.LogInformation("Deleting dead {2} {0} {1}.", item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name, item.GetType().Name);
+            numComplete++;
+            double percent = numComplete;
+            percent /= count;
+            percent *= 100;
 
 
-                _libraryManager.DeleteItem(
-                    item,
-                    new DeleteOptions
-                    {
-                        DeleteFileLocation = false
-                    },
-                    false);
+            progress.Report(percent);
+        }
+
+        var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = [BaseItemKind.MusicArtist],
+            IsDeadArtist = true,
+            IsLocked = false
+        }).Cast<MusicArtist>().ToList();
+
+        foreach (var item in deadEntities)
+        {
+            if (!item.IsAccessedByName)
+            {
+                continue;
             }
             }
 
 
-            progress.Report(100);
+            _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+            _libraryManager.DeleteItem(
+                item,
+                new DeleteOptions
+                {
+                    DeleteFileLocation = false
+                },
+                false);
         }
         }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 107 - 110
Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs

@@ -4,153 +4,150 @@ using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
-using MediaBrowser.Model.Entities;
-using MediaBrowser.Model.Querying;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class CollectionPostScanTask.
+/// </summary>
+public class CollectionPostScanTask : ILibraryPostScanTask
 {
 {
+    private readonly ILibraryManager _libraryManager;
+    private readonly ICollectionManager _collectionManager;
+    private readonly ILogger<CollectionPostScanTask> _logger;
+
     /// <summary>
     /// <summary>
-    /// Class CollectionPostScanTask.
+    /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
     /// </summary>
     /// </summary>
-    public class CollectionPostScanTask : ILibraryPostScanTask
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="collectionManager">The collection manager.</param>
+    /// <param name="logger">The logger.</param>
+    public CollectionPostScanTask(
+        ILibraryManager libraryManager,
+        ICollectionManager collectionManager,
+        ILogger<CollectionPostScanTask> logger)
     {
     {
-        private readonly ILibraryManager _libraryManager;
-        private readonly ICollectionManager _collectionManager;
-        private readonly ILogger<CollectionPostScanTask> _logger;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="CollectionPostScanTask" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="collectionManager">The collection manager.</param>
-        /// <param name="logger">The logger.</param>
-        public CollectionPostScanTask(
-            ILibraryManager libraryManager,
-            ICollectionManager collectionManager,
-            ILogger<CollectionPostScanTask> logger)
-        {
-            _libraryManager = libraryManager;
-            _collectionManager = collectionManager;
-            _logger = logger;
-        }
+        _libraryManager = libraryManager;
+        _collectionManager = collectionManager;
+        _logger = logger;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        var collectionNameMoviesMap = new Dictionary<string, HashSet<Guid>>();
 
 
-            foreach (var library in _libraryManager.RootFolder.Children)
+        foreach (var library in _libraryManager.RootFolder.Children)
+        {
+            if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
             {
             {
-                if (!_libraryManager.GetLibraryOptions(library).AutomaticallyAddToCollection)
-                {
-                    continue;
-                }
+                continue;
+            }
 
 
-                var startIndex = 0;
-                var pagesize = 1000;
+            var startIndex = 0;
+            var pagesize = 1000;
 
 
-                while (true)
+            while (true)
+            {
+                var movies = _libraryManager.GetItemList(new InternalItemsQuery
                 {
                 {
-                    var movies = _libraryManager.GetItemList(new InternalItemsQuery
-                    {
-                        MediaTypes = new[] { MediaType.Video },
-                        IncludeItemTypes = new[] { BaseItemKind.Movie },
-                        IsVirtualItem = false,
-                        OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
-                        Parent = library,
-                        StartIndex = startIndex,
-                        Limit = pagesize,
-                        Recursive = true
-                    });
-
-                    foreach (var m in movies)
+                    MediaTypes = [MediaType.Video],
+                    IncludeItemTypes = [BaseItemKind.Movie],
+                    IsVirtualItem = false,
+                    OrderBy = [(ItemSortBy.SortName, SortOrder.Ascending)],
+                    Parent = library,
+                    StartIndex = startIndex,
+                    Limit = pagesize,
+                    Recursive = true
+                });
+
+                foreach (var m in movies)
+                {
+                    if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
                     {
                     {
-                        if (m is Movie movie && !string.IsNullOrEmpty(movie.CollectionName))
+                        if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
                         {
                         {
-                            if (collectionNameMoviesMap.TryGetValue(movie.CollectionName, out var movieList))
-                            {
-                                movieList.Add(movie.Id);
-                            }
-                            else
-                            {
-                                collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
-                            }
+                            movieList.Add(movie.Id);
+                        }
+                        else
+                        {
+                            collectionNameMoviesMap[movie.CollectionName] = new HashSet<Guid> { movie.Id };
                         }
                         }
                     }
                     }
+                }
 
 
-                    if (movies.Count < pagesize)
-                    {
-                        break;
-                    }
-
-                    startIndex += pagesize;
+                if (movies.Count < pagesize)
+                {
+                    break;
                 }
                 }
+
+                startIndex += pagesize;
             }
             }
+        }
 
 
-            var numComplete = 0;
-            var count = collectionNameMoviesMap.Count;
+        var numComplete = 0;
+        var count = collectionNameMoviesMap.Count;
 
 
-            if (count == 0)
-            {
-                progress.Report(100);
-                return;
-            }
+        if (count == 0)
+        {
+            progress.Report(100);
+            return;
+        }
 
 
-            var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
-            {
-                IncludeItemTypes = new[] { BaseItemKind.BoxSet },
-                CollapseBoxSetItems = false,
-                Recursive = true
-            });
+        var boxSets = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = [BaseItemKind.BoxSet],
+            CollapseBoxSetItems = false,
+            Recursive = true
+        });
 
 
-            foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+        foreach (var (collectionName, movieIds) in collectionNameMoviesMap)
+        {
+            try
             {
             {
-                try
+                var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
+                if (boxSet is null)
                 {
                 {
-                    var boxSet = boxSets.FirstOrDefault(b => b?.Name == collectionName) as BoxSet;
-                    if (boxSet is null)
+                    // won't automatically create collection if only one movie in it
+                    if (movieIds.Count >= 2)
                     {
                     {
-                        // won't automatically create collection if only one movie in it
-                        if (movieIds.Count >= 2)
+                        boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
                         {
                         {
-                            boxSet = await _collectionManager.CreateCollectionAsync(new CollectionCreationOptions
-                            {
-                                Name = collectionName,
-                                IsLocked = true
-                            });
+                            Name = collectionName,
+                        }).ConfigureAwait(false);
 
 
-                            await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
-                        }
+                        await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
                     }
                     }
-                    else
-                    {
-                        await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds);
-                    }
-
-                    numComplete++;
-                    double percent = numComplete;
-                    percent /= count;
-                    percent *= 100;
-
-                    progress.Report(percent);
                 }
                 }
-                catch (Exception ex)
+                else
                 {
                 {
-                    _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+                    await _collectionManager.AddToCollectionAsync(boxSet.Id, movieIds).ConfigureAwait(false);
                 }
                 }
-            }
 
 
-            progress.Report(100);
+                numComplete++;
+                double percent = numComplete;
+                percent /= count;
+                percent *= 100;
+
+                progress.Report(percent);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error refreshing {CollectionName} with {@MovieIds}", collectionName, movieIds);
+            }
         }
         }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 34 - 35
Emby.Server.Implementations/Library/Validators/GenresPostScanTask.cs

@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresPostScanTask.
+/// </summary>
+public class GenresPostScanTask : ILibraryPostScanTask
 {
 {
     /// <summary>
     /// <summary>
-    /// Class GenresPostScanTask.
+    /// The _library manager.
     /// </summary>
     /// </summary>
-    public class GenresPostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<GenresValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    private readonly ILibraryManager _libraryManager;
+    private readonly ILogger<GenresValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public GenresPostScanTask(
-            ILibraryManager libraryManager,
-            ILogger<GenresValidator> logger,
-            IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="GenresPostScanTask" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public GenresPostScanTask(
+        ILibraryManager libraryManager,
+        ILogger<GenresValidator> logger,
+        IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
-        }
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        return new GenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
     }
     }
 }
 }

+ 79 - 57
Emby.Server.Implementations/Library/Validators/GenresValidator.cs

@@ -1,81 +1,103 @@
 using System;
 using System;
+using System.Globalization;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Data.Enums;
+using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class GenresValidator.
+/// </summary>
+public class GenresValidator
 {
 {
     /// <summary>
     /// <summary>
-    /// Class GenresValidator.
+    /// The library manager.
+    /// </summary>
+    private readonly ILibraryManager _libraryManager;
+    private readonly IItemRepository _itemRepo;
+
+    /// <summary>
+    /// The logger.
     /// </summary>
     /// </summary>
-    public class GenresValidator
+    private readonly ILogger<GenresValidator> _logger;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="GenresValidator"/> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
     {
     {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly IItemRepository _itemRepo;
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<GenresValidator> _logger;
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        var names = _itemRepo.GetGenreNames();
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="GenresValidator"/> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public GenresValidator(ILibraryManager libraryManager, ILogger<GenresValidator> logger, IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+        var numComplete = 0;
+        var count = names.Count;
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+        foreach (var name in names)
         {
         {
-            var names = _itemRepo.GetGenreNames();
-
-            var numComplete = 0;
-            var count = names.Count;
+            try
+            {
+                var item = _libraryManager.GetGenre(name);
 
 
-            foreach (var name in names)
+                await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException)
             {
             {
-                try
-                {
-                    var item = _libraryManager.GetGenre(name);
+                // Don't clutter the log
+                throw;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error refreshing {GenreName}", name);
+            }
 
 
-                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException)
-                {
-                    // Don't clutter the log
-                    throw;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error refreshing {GenreName}", name);
-                }
+            numComplete++;
+            double percent = numComplete;
+            percent /= count;
+            percent *= 100;
 
 
-                numComplete++;
-                double percent = numComplete;
-                percent /= count;
-                percent *= 100;
+            progress.Report(percent);
+        }
 
 
-                progress.Report(percent);
-            }
+        var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = [BaseItemKind.Genre, BaseItemKind.MusicGenre],
+            IsDeadGenre = true,
+            IsLocked = false
+        });
 
 
-            progress.Report(100);
+        foreach (var item in deadEntities)
+        {
+            _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+            _libraryManager.DeleteItem(
+                item,
+                new DeleteOptions
+                {
+                    DeleteFileLocation = false
+                },
+                false);
         }
         }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 34 - 35
Emby.Server.Implementations/Library/Validators/MusicGenresPostScanTask.cs

@@ -5,45 +5,44 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class MusicGenresPostScanTask : ILibraryPostScanTask
 {
 {
     /// <summary>
     /// <summary>
-    /// Class MusicGenresPostScanTask.
+    /// The library manager.
     /// </summary>
     /// </summary>
-    public class MusicGenresPostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-        private readonly ILogger<MusicGenresValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    private readonly ILibraryManager _libraryManager;
+    private readonly ILogger<MusicGenresValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public MusicGenresPostScanTask(
-            ILibraryManager libraryManager,
-            ILogger<MusicGenresValidator> logger,
-            IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MusicGenresPostScanTask" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public MusicGenresPostScanTask(
+        ILibraryManager libraryManager,
+        ILogger<MusicGenresValidator> logger,
+        IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
-        }
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        return new MusicGenresValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
     }
     }
 }
 }

+ 58 - 59
Emby.Server.Implementations/Library/Validators/MusicGenresValidator.cs

@@ -5,77 +5,76 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresValidator.
+/// </summary>
+public class MusicGenresValidator
 {
 {
     /// <summary>
     /// <summary>
-    /// Class MusicGenresValidator.
+    /// The library manager.
     /// </summary>
     /// </summary>
-    public class MusicGenresValidator
-    {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
+    private readonly ILibraryManager _libraryManager;
 
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<MusicGenresValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    /// <summary>
+    /// The logger.
+    /// </summary>
+    private readonly ILogger<MusicGenresValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="MusicGenresValidator" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public MusicGenresValidator(ILibraryManager libraryManager, ILogger<MusicGenresValidator> logger, IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            var names = _itemRepo.GetMusicGenreNames();
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        var names = _itemRepo.GetMusicGenreNames();
 
 
-            var numComplete = 0;
-            var count = names.Count;
+        var numComplete = 0;
+        var count = names.Count;
 
 
-            foreach (var name in names)
+        foreach (var name in names)
+        {
+            try
             {
             {
-                try
-                {
-                    var item = _libraryManager.GetMusicGenre(name);
+                var item = _libraryManager.GetMusicGenre(name);
 
 
-                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException)
-                {
-                    // Don't clutter the log
-                    throw;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error refreshing {GenreName}", name);
-                }
-
-                numComplete++;
-                double percent = numComplete;
-                percent /= count;
-                percent *= 100;
-
-                progress.Report(percent);
+                await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException)
+            {
+                // Don't clutter the log
+                throw;
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error refreshing {GenreName}", name);
             }
             }
 
 
-            progress.Report(100);
+            numComplete++;
+            double percent = numComplete;
+            percent /= count;
+            percent *= 100;
+
+            progress.Report(percent);
         }
         }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 88 - 93
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -9,119 +9,114 @@ using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class PeopleValidator.
+/// </summary>
+public class PeopleValidator
 {
 {
     /// <summary>
     /// <summary>
-    /// Class PeopleValidator.
+    /// The _library manager.
     /// </summary>
     /// </summary>
-    public class PeopleValidator
+    private readonly ILibraryManager _libraryManager;
+
+    /// <summary>
+    /// The _logger.
+    /// </summary>
+    private readonly ILogger _logger;
+
+    private readonly IFileSystem _fileSystem;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="fileSystem">The file system.</param>
+    public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
     {
     {
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
-
-        /// <summary>
-        /// The _logger.
-        /// </summary>
-        private readonly ILogger _logger;
-
-        private readonly IFileSystem _fileSystem;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PeopleValidator" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="fileSystem">The file system.</param>
-        public PeopleValidator(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _fileSystem = fileSystem;
-        }
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _fileSystem = fileSystem;
+    }
 
 
-        /// <summary>
-        /// Validates the people.
-        /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <param name="progress">The progress.</param>
-        /// <returns>Task.</returns>
-        public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
-        {
-            var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
+    /// <summary>
+    /// Validates the people.
+    /// </summary>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <param name="progress">The progress.</param>
+    /// <returns>Task.</returns>
+    public async Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress)
+    {
+        var people = _libraryManager.GetPeopleNames(new InternalPeopleQuery());
 
 
-            var numComplete = 0;
+        var numComplete = 0;
 
 
-            var numPeople = people.Count;
+        var numPeople = people.Count;
 
 
-            _logger.LogDebug("Will refresh {0} people", numPeople);
+        _logger.LogDebug("Will refresh {Amount} people", numPeople);
 
 
-            foreach (var person in people)
-            {
-                cancellationToken.ThrowIfCancellationRequested();
+        foreach (var person in people)
+        {
+            cancellationToken.ThrowIfCancellationRequested();
 
 
-                try
-                {
-                    var item = _libraryManager.GetPerson(person);
-                    if (item is null)
-                    {
-                        _logger.LogWarning("Failed to get person: {Name}", person);
-                        continue;
-                    }
-
-                    var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
-                    {
-                        ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
-                        MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
-                    };
-
-                    await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException)
-                {
-                    throw;
-                }
-                catch (Exception ex)
+            try
+            {
+                var item = _libraryManager.GetPerson(person);
+                if (item is null)
                 {
                 {
-                    _logger.LogError(ex, "Error validating IBN entry {Person}", person);
+                    _logger.LogWarning("Failed to get person: {Name}", person);
+                    continue;
                 }
                 }
 
 
-                // Update progress
-                numComplete++;
-                double percent = numComplete;
-                percent /= numPeople;
+                var options = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                {
+                    ImageRefreshMode = MetadataRefreshMode.ValidationOnly,
+                    MetadataRefreshMode = MetadataRefreshMode.ValidationOnly
+                };
 
 
-                progress.Report(100 * percent);
+                await item.RefreshMetadata(options, cancellationToken).ConfigureAwait(false);
             }
             }
-
-            var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+            catch (OperationCanceledException)
             {
             {
-                IncludeItemTypes = [BaseItemKind.Person],
-                IsDeadPerson = true,
-                IsLocked = false
-            });
-
-            foreach (var item in deadEntities)
+                throw;
+            }
+            catch (Exception ex)
             {
             {
-                _logger.LogInformation(
-                    "Deleting dead {2} {0} {1}.",
-                    item.Id.ToString("N", CultureInfo.InvariantCulture),
-                    item.Name,
-                    item.GetType().Name);
-
-                _libraryManager.DeleteItem(
-                    item,
-                    new DeleteOptions
-                    {
-                        DeleteFileLocation = false
-                    },
-                    false);
+                _logger.LogError(ex, "Error validating IBN entry {Person}", person);
             }
             }
 
 
-            progress.Report(100);
+            // Update progress
+            numComplete++;
+            double percent = numComplete;
+            percent /= numPeople;
 
 
-            _logger.LogInformation("People validation complete");
+            progress.Report(100 * percent);
         }
         }
+
+        var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = [BaseItemKind.Person],
+            IsDeadPerson = true,
+            IsLocked = false
+        });
+
+        foreach (var item in deadEntities)
+        {
+            _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+            _libraryManager.DeleteItem(
+                item,
+                new DeleteOptions
+                {
+                    DeleteFileLocation = false
+                },
+                false);
+        }
+
+        progress.Report(100);
+
+        _logger.LogInformation("People validation complete");
     }
     }
 }
 }

+ 34 - 35
Emby.Server.Implementations/Library/Validators/StudiosPostScanTask.cs

@@ -5,46 +5,45 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class MusicGenresPostScanTask.
+/// </summary>
+public class StudiosPostScanTask : ILibraryPostScanTask
 {
 {
     /// <summary>
     /// <summary>
-    /// Class MusicGenresPostScanTask.
+    /// The _library manager.
     /// </summary>
     /// </summary>
-    public class StudiosPostScanTask : ILibraryPostScanTask
-    {
-        /// <summary>
-        /// The _library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
+    private readonly ILibraryManager _libraryManager;
 
 
-        private readonly ILogger<StudiosValidator> _logger;
-        private readonly IItemRepository _itemRepo;
+    private readonly ILogger<StudiosValidator> _logger;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public StudiosPostScanTask(
-            ILibraryManager libraryManager,
-            ILogger<StudiosValidator> logger,
-            IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="StudiosPostScanTask" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public StudiosPostScanTask(
+        ILibraryManager libraryManager,
+        ILogger<StudiosValidator> logger,
+        IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
-        }
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        return new StudiosValidator(_libraryManager, _logger, _itemRepo).Run(progress, cancellationToken);
     }
     }
 }
 }

+ 75 - 76
Emby.Server.Implementations/Library/Validators/StudiosValidator.cs

@@ -8,98 +8,97 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
-namespace Emby.Server.Implementations.Library.Validators
+namespace Emby.Server.Implementations.Library.Validators;
+
+/// <summary>
+/// Class StudiosValidator.
+/// </summary>
+public class StudiosValidator
 {
 {
     /// <summary>
     /// <summary>
-    /// Class StudiosValidator.
+    /// The library manager.
     /// </summary>
     /// </summary>
-    public class StudiosValidator
-    {
-        /// <summary>
-        /// The library manager.
-        /// </summary>
-        private readonly ILibraryManager _libraryManager;
+    private readonly ILibraryManager _libraryManager;
 
 
-        private readonly IItemRepository _itemRepo;
+    private readonly IItemRepository _itemRepo;
 
 
-        /// <summary>
-        /// The logger.
-        /// </summary>
-        private readonly ILogger<StudiosValidator> _logger;
+    /// <summary>
+    /// The logger.
+    /// </summary>
+    private readonly ILogger<StudiosValidator> _logger;
 
 
-        /// <summary>
-        /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
-        /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="logger">The logger.</param>
-        /// <param name="itemRepo">The item repository.</param>
-        public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
-        {
-            _libraryManager = libraryManager;
-            _logger = logger;
-            _itemRepo = itemRepo;
-        }
+    /// <summary>
+    /// Initializes a new instance of the <see cref="StudiosValidator" /> class.
+    /// </summary>
+    /// <param name="libraryManager">The library manager.</param>
+    /// <param name="logger">The logger.</param>
+    /// <param name="itemRepo">The item repository.</param>
+    public StudiosValidator(ILibraryManager libraryManager, ILogger<StudiosValidator> logger, IItemRepository itemRepo)
+    {
+        _libraryManager = libraryManager;
+        _logger = logger;
+        _itemRepo = itemRepo;
+    }
 
 
-        /// <summary>
-        /// Runs the specified progress.
-        /// </summary>
-        /// <param name="progress">The progress.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
-        {
-            var names = _itemRepo.GetStudioNames();
+    /// <summary>
+    /// Runs the specified progress.
+    /// </summary>
+    /// <param name="progress">The progress.</param>
+    /// <param name="cancellationToken">The cancellation token.</param>
+    /// <returns>Task.</returns>
+    public async Task Run(IProgress<double> progress, CancellationToken cancellationToken)
+    {
+        var names = _itemRepo.GetStudioNames();
 
 
-            var numComplete = 0;
-            var count = names.Count;
+        var numComplete = 0;
+        var count = names.Count;
 
 
-            foreach (var name in names)
+        foreach (var name in names)
+        {
+            try
             {
             {
-                try
-                {
-                    var item = _libraryManager.GetStudio(name);
+                var item = _libraryManager.GetStudio(name);
 
 
-                    await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
-                }
-                catch (OperationCanceledException)
-                {
-                    // Don't clutter the log
-                    throw;
-                }
-                catch (Exception ex)
-                {
-                    _logger.LogError(ex, "Error refreshing {StudioName}", name);
-                }
-
-                numComplete++;
-                double percent = numComplete;
-                percent /= count;
-                percent *= 100;
-
-                progress.Report(percent);
+                await item.RefreshMetadata(cancellationToken).ConfigureAwait(false);
             }
             }
-
-            var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+            catch (OperationCanceledException)
             {
             {
-                IncludeItemTypes = new[] { BaseItemKind.Studio },
-                IsDeadStudio = true,
-                IsLocked = false
-            });
-
-            foreach (var item in deadEntities)
+                // Don't clutter the log
+                throw;
+            }
+            catch (Exception ex)
             {
             {
-                _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
-
-                _libraryManager.DeleteItem(
-                    item,
-                    new DeleteOptions
-                    {
-                        DeleteFileLocation = false
-                    },
-                    false);
+                _logger.LogError(ex, "Error refreshing {StudioName}", name);
             }
             }
 
 
-            progress.Report(100);
+            numComplete++;
+            double percent = numComplete;
+            percent /= count;
+            percent *= 100;
+
+            progress.Report(percent);
         }
         }
+
+        var deadEntities = _libraryManager.GetItemList(new InternalItemsQuery
+        {
+            IncludeItemTypes = [BaseItemKind.Studio],
+            IsDeadStudio = true,
+            IsLocked = false
+        });
+
+        foreach (var item in deadEntities)
+        {
+            _logger.LogInformation("Deleting dead {ItemType} {ItemId} {ItemName}", item.GetType().Name, item.Id.ToString("N", CultureInfo.InvariantCulture), item.Name);
+
+            _libraryManager.DeleteItem(
+                item,
+                new DeleteOptions
+                {
+                    DeleteFileLocation = false
+                },
+                false);
+        }
+
+        progress.Report(100);
     }
     }
 }
 }

+ 7 - 1
Emby.Server.Implementations/Localization/Core/af.json

@@ -129,5 +129,11 @@
     "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
     "TaskAudioNormalizationDescription": "Skandeer lêers vir oudio-normaliseringsdata.",
     "TaskAudioNormalization": "Odio Normalisering",
     "TaskAudioNormalization": "Odio Normalisering",
     "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
     "TaskCleanCollectionsAndPlaylists": "Maak versamelings en snitlyste skoon",
-    "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie."
+    "TaskCleanCollectionsAndPlaylistsDescription": "Verwyder items uit versamelings en snitlyste wat nie meer bestaan nie.",
+    "TaskDownloadMissingLyrics": "Laai tekorte lirieke af",
+    "TaskDownloadMissingLyricsDescription": "Laai lirieke af vir liedjies",
+    "TaskExtractMediaSegments": "Media Segment Skandeer",
+    "TaskExtractMediaSegmentsDescription": "Onttrek of verkry mediasegmente van MediaSegment-geaktiveerde inproppe.",
+    "TaskMoveTrickplayImages": "Migreer Trickplay Beeldligging",
+    "TaskMoveTrickplayImagesDescription": "Skuif ontstaande trickplay lêers volgens die biblioteekinstellings."
 }
 }

+ 5 - 3
Emby.Server.Implementations/Localization/Core/ar.json

@@ -125,8 +125,8 @@
     "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
     "TaskKeyframeExtractor": "مستخرج الإطار الرئيسي",
     "External": "خارجي",
     "External": "خارجي",
     "HearingImpaired": "ضعاف السمع",
     "HearingImpaired": "ضعاف السمع",
-    "TaskRefreshTrickplayImages": "توليد صور Trickplay",
-    "TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
+    "TaskRefreshTrickplayImages": "توليد صور المعاينة السريعة",
+    "TaskRefreshTrickplayImagesDescription": "يُولّد معاينات تنقل سريع لمقاطع الفيديو ضمن المكتبات المفعّلة.",
     "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
     "TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
     "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
     "TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة.",
     "TaskAudioNormalization": "تسوية الصوت",
     "TaskAudioNormalization": "تسوية الصوت",
@@ -136,5 +136,7 @@
     "TaskExtractMediaSegments": "فحص مقاطع الوسائط",
     "TaskExtractMediaSegments": "فحص مقاطع الوسائط",
     "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
     "TaskExtractMediaSegmentsDescription": "يستخرج مقاطع وسائط من إضافات MediaSegment المُفعّلة.",
     "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
     "TaskMoveTrickplayImages": "تغيير مكان صور المعاينة السريعة",
-    "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة."
+    "TaskMoveTrickplayImagesDescription": "تُنقل ملفات التشغيل السريع الحالية بناءً على إعدادات المكتبة.",
+    "CleanupUserDataTask": "مهمة تنظيف بيانات المستخدم",
+    "CleanupUserDataTaskDescription": "مسح جميع بيانات المستخدم (حالة المشاهدة، والحالة المفضلة وما إلى ذلك) من الوسائط التي لم تعد موجودة لمدة 90 يومًا على الأقل."
 }
 }

+ 5 - 3
Emby.Server.Implementations/Localization/Core/be.json

@@ -1,6 +1,6 @@
 {
 {
     "Sync": "Сінхранізаваць",
     "Sync": "Сінхранізаваць",
-    "Playlists": "Плэйлісты",
+    "Playlists": "Спісы прайгравання",
     "Latest": "Апошні",
     "Latest": "Апошні",
     "LabelIpAddressValue": "IP-адрас: {0}",
     "LabelIpAddressValue": "IP-адрас: {0}",
     "ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
     "ItemAddedWithName": "{0} быў дададзены ў бібліятэку",
@@ -16,7 +16,7 @@
     "Collections": "Калекцыі",
     "Collections": "Калекцыі",
     "Default": "Па змаўчанні",
     "Default": "Па змаўчанні",
     "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
     "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}",
-    "Folders": "Папкі",
+    "Folders": "Тэчкі",
     "Favorites": "Абранае",
     "Favorites": "Абранае",
     "External": "Знешні",
     "External": "Знешні",
     "Genres": "Жанры",
     "Genres": "Жанры",
@@ -135,5 +135,7 @@
     "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
     "TaskDownloadMissingLyrics": "Спампаваць зніклыя тэксты песень",
     "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
     "TaskDownloadMissingLyricsDescription": "Спампоўвае тэксты для песень",
     "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
     "TaskExtractMediaSegments": "Сканіраванне медыя-сегмента",
-    "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay"
+    "TaskMoveTrickplayImages": "Перанесці месцазнаходжанне выявы Trickplay",
+    "CleanupUserDataTask": "Задача па ачыстцы дадзеных карыстальніка",
+    "CleanupUserDataTaskDescription": "Ачысьціць усе дадзеныя карыстальніка (стан прагляду, абранае і г.д.) для медыяфайлаў, што адсутнічаюць больш за 90 дзён."
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
     "TaskExtractMediaSegmentsDescription": "Изважда медиини сегменти от MediaSegment плъгини.",
     "TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
     "TaskMoveTrickplayImages": "Мигриране на Локацията за Trickplay изображения",
     "TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
     "TaskMoveTrickplayImagesDescription": "Премества съществуващите trickplay изображения спрямо настройките на библиотеката.",
-    "TaskExtractMediaSegments": "Сканиране за сегменти"
+    "TaskExtractMediaSegments": "Сканиране за сегменти",
+    "CleanupUserDataTask": "Задача за почистване на потребителски данни",
+    "CleanupUserDataTaskDescription": "Почиства всички потребителски данни (статус на гледане, любими и т.н.) от медия, която вече не е налична от поне 90 дни."
 }
 }

+ 47 - 41
Emby.Server.Implementations/Localization/Core/bn.json

@@ -6,29 +6,29 @@
     "Channels": "চ্যানেলসমূহ",
     "Channels": "চ্যানেলসমূহ",
     "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
     "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
     "Books": "পুস্তকসমূহ",
     "Books": "পুস্তকসমূহ",
-    "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
+    "AuthenticationSucceededWithUserName": "{0} সফলভাবে অথেন্টিকেট করেছেন",
     "Artists": "শিল্পীগণ",
     "Artists": "শিল্পীগণ",
     "Application": "অ্যাপ্লিকেশন",
     "Application": "অ্যাপ্লিকেশন",
     "Albums": "অ্যালবামসমূহ",
     "Albums": "অ্যালবামসমূহ",
-    "HeaderFavoriteEpisodes": "প্রি পর্বগুলো",
+    "HeaderFavoriteEpisodes": "প্রিয় পর্বগুলো",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
     "HeaderContinueWatching": "দেখতে থাকুন",
     "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
     "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
-    "Genres": "শৈলীধারাসমূহ",
+    "Genres": "জনরা",
     "Folders": "ফোল্ডারসমূহ",
     "Folders": "ফোল্ডারসমূহ",
     "Favorites": "পছন্দসমূহ",
     "Favorites": "পছন্দসমূহ",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
-    "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
+    "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {1}",
     "VersionNumber": "সংস্করণ {0}",
     "VersionNumber": "সংস্করণ {0}",
     "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
     "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
-    "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
-    "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
+    "UserStoppedPlayingItemWithValues": "{2}তে {1} প্লে শেষ করেছেন {0}",
+    "UserStartedPlayingItemWithValues": "{2}তে {1} প্লে করেছেন {0}",
     "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
     "UserPolicyUpdatedWithName": "{0} এর জন্য ব্যবহার নীতি আপডেট করা হয়েছে",
     "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
     "UserPasswordChangedWithName": "ব্যবহারকারী {0} এর পাসওয়ার্ড পরিবর্তিত হয়েছে",
-    "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন",
-    "UserOfflineFromDevice": "{0} {1} থেকে বিযুক্ত হয়ে গেছে",
+    "UserOnlineFromDevice": "{0}, {1} থেকে অনলাইন আছে",
+    "UserOfflineFromDevice": "{0} {1} থেকে বিচ্ছিন্ন হয়ে গেছে",
     "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
     "UserLockedOutWithName": "ব্যবহারকারী {0} ঢুকতে পারছে না",
     "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
     "UserDownloadingItemWithValues": "{0}, {1} ডাউনলোড করছে",
     "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
     "UserDeletedWithName": "ব্যবহারকারী {0}কে বাদ দেয়া হয়েছে",
@@ -36,8 +36,8 @@
     "User": "ব্যবহারকারী",
     "User": "ব্যবহারকারী",
     "TvShows": "টিভি শোগুলো",
     "TvShows": "টিভি শোগুলো",
     "System": "সিস্টেম",
     "System": "সিস্টেম",
-    "Sync": "সমলয় স্থাপন",
-    "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
+    "Sync": "সমন্বয় করুন",
+    "SubtitleDownloadFailureFromForItem": "{0} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ হয়েছে",
     "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
     "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
     "Songs": "সঙ্গীতসমূহ",
     "Songs": "সঙ্গীতসমূহ",
     "Shows": "টিভি পর্ব",
     "Shows": "টিভি পর্ব",
@@ -46,18 +46,18 @@
     "ScheduledTaskFailedWithName": "{0} ব্যর্থ",
     "ScheduledTaskFailedWithName": "{0} ব্যর্থ",
     "ProviderValue": "প্রদানকারী: {0}",
     "ProviderValue": "প্রদানকারী: {0}",
     "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
     "PluginUpdatedWithName": "{0} আপডেট করা হয়েছে",
-    "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
-    "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
+    "PluginUninstalledWithName": "{0} আনইন্সটল হয়েছে",
+    "PluginInstalledWithName": "{0} ইন্সটল হয়েছে",
     "Plugin": "প্লাগিন",
     "Plugin": "প্লাগিন",
     "Playlists": "প্লে লিস্ট সমূহ",
     "Playlists": "প্লে লিস্ট সমূহ",
-    "Photos": "চিত্রসমূহ",
-    "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
-    "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
+    "Photos": "ছবিসমূহ",
+    "NotificationOptionVideoPlaybackStopped": "ভিডিও বন্ধ হয়েছে",
+    "NotificationOptionVideoPlayback": "ভিডিও শুরু হয়েছে",
     "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
     "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
     "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
     "NotificationOptionTaskFailed": "পরিকল্পিত কাজটি ব্যর্থ",
-    "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট বাধ্যতামূলক",
-    "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল করা হয়েছে",
-    "NotificationOptionPluginUninstalled": "প্লাগিন বাদ দেয়া হয়েছে",
+    "NotificationOptionServerRestartRequired": "সার্ভার রিস্টার্ট করা লাগবে",
+    "NotificationOptionPluginUpdateInstalled": "প্লাগিন আপডেট ইন্সটল হয়েছে",
+    "NotificationOptionPluginUninstalled": "প্লাগিন আনইনষ্টল হয়েছে",
     "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
     "NotificationOptionPluginInstalled": "প্লাগিন ইন্সটল করা হয়েছে",
     "NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
     "NotificationOptionPluginError": "প্লাগিন ব্যর্থ",
     "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
     "NotificationOptionNewLibraryContent": "নতুন কন্টেন্ট যোগ করা হয়েছে",
@@ -76,8 +76,8 @@
     "Movies": "চলচ্চিত্রসমূহ",
     "Movies": "চলচ্চিত্রসমূহ",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
     "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
-    "HeaderRecordingGroups": "রেকর্ডিং দল",
-    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভারের {0} কনফিগারেসনের অংশ আপডেট করা হয়েছে",
+    "HeaderRecordingGroups": "রেকর্ডিং গ্রুপগুলো",
+    "MessageNamedServerConfigurationUpdatedWithValue": "সার্ভার কনফিগারেশন সেকশন {0} আপডেট করা হয়েছে",
     "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
     "MessageApplicationUpdatedTo": "জেলিফিন সার্ভার {0} তে আপডেট করা হয়েছে",
     "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
     "MessageApplicationUpdated": "জেলিফিন সার্ভার আপডেট করা হয়েছে",
     "Latest": "সর্বশেষ",
     "Latest": "সর্বশেষ",
@@ -85,51 +85,57 @@
     "LabelIpAddressValue": "আইপি এড্রেস: {0}",
     "LabelIpAddressValue": "আইপি এড্রেস: {0}",
     "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
     "ItemRemovedWithName": "{0} লাইব্রেরি থেকে বাদ দেয়া হয়েছে",
     "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
     "ItemAddedWithName": "{0} লাইব্রেরিতে যোগ করা হয়েছে",
-    "Inherit": "থেকে পাওয়া",
+    "Inherit": "মূল থেকে গ্রহণ করুন",
     "HomeVideos": "হোম ভিডিও",
     "HomeVideos": "হোম ভিডিও",
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderNextUp": "এরপরে আসছে",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderLiveTV": "লাইভ টিভি",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",
     "HeaderFavoriteSongs": "প্রিয় গানগুলো",
     "HeaderFavoriteShows": "প্রিয় শোগুলো",
     "HeaderFavoriteShows": "প্রিয় শোগুলো",
-    "TasksLibraryCategory": "গ্রন্থাগার",
+    "TasksLibraryCategory": "লাইব্রেরি",
     "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
     "TasksMaintenanceCategory": "রক্ষণাবেক্ষণ",
     "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
     "TaskRefreshLibrary": "স্ক্যান মিডিয়া লাইব্রেরি",
-    "TaskRefreshChapterImagesDescription": "অধ্যায়গুলিতে থাকা ভিডিওগুলির জন্য থাম্বনেইল তৈরি ।",
-    "TaskRefreshChapterImages": "অধ্যায়ের চিত্রগুলি বের করুন",
-    "TaskCleanCacheDescription": "সিস্টেমে আর প্রয়োজন নেই ক্যাশ, ফাইলগুলি মুছে ফেলুন।",
+    "TaskRefreshChapterImagesDescription": "যেসব ভিডিওতে চ্যাপ্টার রয়েছে, তাদের জন্য থাম্বনেইল তৈরি করবে।",
+    "TaskRefreshChapterImages": "চ্যাপ্টার ইমেজ বের করুন",
+    "TaskCleanCacheDescription": "সিস্টেমের অপ্রয়োজনীয় ক্যাশ ফাইলগুলো মুছে ফেলবে।",
     "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
     "TaskCleanCache": "ক্লিন ক্যাশ ডিরেক্টরি",
     "TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
     "TasksChannelsCategory": "ইন্টারনেট চ্যানেল",
-    "TasksApplicationCategory": "আবেদন",
+    "TasksApplicationCategory": "অ্যাপ্লিকেশন",
     "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
     "TaskDownloadMissingSubtitlesDescription": "মেটাডেটা কনফিগারেশনের উপর ভিত্তি করে অনুপস্থিত সাবটাইটেলগুলির জন্য ইন্টারনেট অনুসন্ধান করে।",
     "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
     "TaskDownloadMissingSubtitles": "অনুপস্থিত সাবটাইটেলগুলি ডাউনলোড করুন",
     "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
     "TaskRefreshChannelsDescription": "ইন্টারনেট চ্যানেল তথ্য রিফ্রেশ করুন।",
     "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
     "TaskRefreshChannels": "চ্যানেল রিফ্রেশ করুন",
-    "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলুন।",
+    "TaskCleanTranscodeDescription": "এক দিনেরও বেশি পুরানো ট্রান্সকোড ফাইলগুলি মুছে ফেলবে।",
     "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
     "TaskCleanTranscode": "ট্রান্সকোড ডিরেক্টরি ক্লিন করুন",
     "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
     "TaskUpdatePluginsDescription": "স্বয়ংক্রিয়ভাবে আপডেট কনফিগার করা প্লাগইনগুলির জন্য আপডেট ডাউনলোড এবং ইনস্টল করুন।",
-    "TaskUpdatePlugins": "প্লাগইন আপডেট করুন",
-    "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করুন।",
-    "TaskRefreshPeople": "পিপল রিফ্রেশ করুন",
-    "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলুন।",
-    "TaskCleanLogs": "লগ ডিরেক্টরি ক্লিন করুন",
-    "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করুন।",
+    "TaskUpdatePlugins": "আপডেট প্লাগইন",
+    "TaskRefreshPeopleDescription": "আপনার মিডিয়া লাইব্রেরিতে অভিনেতা এবং পরিচালকদের জন্য মেটাডাটা আপডেট করবে।",
+    "TaskRefreshPeople": "ব্যক্তিদের তথ্য রিফ্রেশ",
+    "TaskCleanLogsDescription": "{0} দিনের বেশী পুরানো লগ ফাইলগুলি মুছে ফেলবে।",
+    "TaskCleanLogs": "ক্লিন লগ ডিরেক্টরি",
+    "TaskRefreshLibraryDescription": "নতুন ফাইলের জন্য মিডিয়া লাইব্রেরি স্ক্যান এবং মেটাডাটা রিফ্রেশ করবে।",
     "Undefined": "অসঙ্গায়িত",
     "Undefined": "অসঙ্গায়িত",
     "Forced": "জোরকরে",
     "Forced": "জোরকরে",
-    "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
-    "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
+    "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের অ্যাক্টিভিটি লগ মুছে দিবে।",
+    "TaskCleanActivityLog": "অ্যাক্টিভিটি লগ মুছুন",
     "Default": "ডিফল্ট",
     "Default": "ডিফল্ট",
-    "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
+    "HearingImpaired": "শ্রবণ প্রতিবন্ধী",
     "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
     "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
     "External": "বাহ্যিক",
     "External": "বাহ্যিক",
     "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
     "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
     "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
     "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
     "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
     "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।",
-    "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি করুন",
+    "TaskRefreshTrickplayImages": "ট্রিকপ্লে ইমেজ তৈরি",
     "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
     "TaskRefreshTrickplayImagesDescription": "সক্ষম লাইব্রেরিতে ভিডিওর জন্য ট্রিকপ্লে প্রিভিউ তৈরি করে।",
     "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
     "TaskDownloadMissingLyricsDescription": "গানের লিরিক্স ডাউনলোড করে",
-    "TaskCleanCollectionsAndPlaylists": "সংগ্রহ এবং প্লেলিস্ট পরিষ্কার করুন",
-    "TaskCleanCollectionsAndPlaylistsDescription": "সংগ্রহ এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
+    "TaskCleanCollectionsAndPlaylists": "কালেকশন এবং প্লেলিস্ট পরিষ্কার করুন",
+    "TaskCleanCollectionsAndPlaylistsDescription": "কালেকশন এবং প্লেলিস্ট থেকে আইটেমগুলি সরিয়ে দেয় যা আর বিদ্যমান নেই।",
     "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
     "TaskExtractMediaSegments": "মিডিয়া সেগমেন্ট স্ক্যান",
-    "TaskExtractMediaSegmentsDescription": "MediaSegment সক্ষম প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
-    "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন"
+    "TaskExtractMediaSegmentsDescription": "মিডিয়া সেগমেন্ট সক্রিয় প্লাগইনগুলি থেকে মিডিয়া সেগমেন্টগুলি বের করে বা প্রাপ্ত করে।",
+    "TaskDownloadMissingLyrics": "অনুপস্থিত গান ডাউনলোড করুন",
+    "TaskMoveTrickplayImagesDescription": "লাইব্রেরির সেটিং অনুযায়ী বিদ্যমান ট্রিকপ্লে ফাইলগুলো সরিয়ে নেবে।",
+    "TaskAudioNormalizationDescription": "অডিও নর্মালাইজেশন তথ্যের জন্য ফাইল স্ক্যান করবে।",
+    "CleanupUserDataTaskDescription": "৯০ দিন বা তার বেশি সময় ধরে অনুপস্থিত মিডিয়া থেকে সকল ব্যবহারকারীর ডেটা (ওয়াচ স্টেট, ফেভারিট স্ট্যাটাস ইত্যাদি) মুছে ফেলবে।",
+    "TaskMoveTrickplayImages": "ট্রিকপ্লে ইমেজের অবস্থান পরিবর্তন",
+    "TaskAudioNormalization": "অডিও নর্মলাইজেশন",
+    "CleanupUserDataTask": "ব্যবহারকারীর ডেটা পরিষ্কারের কাজ"
 }
 }

+ 43 - 41
Emby.Server.Implementations/Localization/Core/ca.json

@@ -13,10 +13,10 @@
     "DeviceOnlineWithName": "{0} està connectat",
     "DeviceOnlineWithName": "{0} està connectat",
     "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
     "FailedLoginAttemptWithUserName": "Intent de connexió fallit des de {0}",
     "Favorites": "Preferits",
     "Favorites": "Preferits",
-    "Folders": "Carpetes",
+    "Folders": "Directoris",
     "Genres": "Gèneres",
     "Genres": "Gèneres",
     "HeaderAlbumArtists": "Artistes de l'àlbum",
     "HeaderAlbumArtists": "Artistes de l'àlbum",
-    "HeaderContinueWatching": "Continua veient",
+    "HeaderContinueWatching": "Continueu mirant",
     "HeaderFavoriteAlbums": "Àlbums preferits",
     "HeaderFavoriteAlbums": "Àlbums preferits",
     "HeaderFavoriteArtists": "Artistes preferits",
     "HeaderFavoriteArtists": "Artistes preferits",
     "HeaderFavoriteEpisodes": "Episodis preferits",
     "HeaderFavoriteEpisodes": "Episodis preferits",
@@ -24,11 +24,11 @@
     "HeaderFavoriteSongs": "Cançons preferides",
     "HeaderFavoriteSongs": "Cançons preferides",
     "HeaderLiveTV": "TV en directe",
     "HeaderLiveTV": "TV en directe",
     "HeaderNextUp": "A continuació",
     "HeaderNextUp": "A continuació",
-    "HeaderRecordingGroups": "Grups Musicals",
+    "HeaderRecordingGroups": "Grups musicals",
     "HomeVideos": "Vídeos domèstics",
     "HomeVideos": "Vídeos domèstics",
     "Inherit": "Heretat",
     "Inherit": "Heretat",
-    "ItemAddedWithName": "{0} s'ha afegit a la biblioteca",
-    "ItemRemovedWithName": "{0} s'ha eliminat de la biblioteca",
+    "ItemAddedWithName": "{0} s'ha afegit a la mediateca",
+    "ItemRemovedWithName": "{0} s'ha eliminat de la mediateca",
     "LabelIpAddressValue": "Adreça IP: {0}",
     "LabelIpAddressValue": "Adreça IP: {0}",
     "LabelRunningTimeValue": "Temps en marxa: {0}",
     "LabelRunningTimeValue": "Temps en marxa: {0}",
     "Latest": "Darrers",
     "Latest": "Darrers",
@@ -43,7 +43,7 @@
     "NameInstallFailed": "{0} instal·lació fallida",
     "NameInstallFailed": "{0} instal·lació fallida",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonUnknown": "Temporada desconeguda",
     "NameSeasonUnknown": "Temporada desconeguda",
-    "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
+    "NewVersionIsAvailable": "Hi ha disponible una versió nova del servidor de Jellyfin per a la descàrrega.",
     "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
     "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicatiu disponible",
     "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
     "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicatiu instal·lada",
     "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
     "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
@@ -64,7 +64,7 @@
     "Playlists": "Llistes de reproducció",
     "Playlists": "Llistes de reproducció",
     "Plugin": "Complement",
     "Plugin": "Complement",
     "PluginInstalledWithName": "{0} ha estat instal·lat",
     "PluginInstalledWithName": "{0} ha estat instal·lat",
-    "PluginUninstalledWithName": "S'ha instalat {0}",
+    "PluginUninstalledWithName": "S'ha instal·lat {0}",
     "PluginUpdatedWithName": "S'ha actualitzat {0}",
     "PluginUpdatedWithName": "S'ha actualitzat {0}",
     "ProviderValue": "Proveïdor: {0}",
     "ProviderValue": "Proveïdor: {0}",
     "ScheduledTaskFailedWithName": "{0} ha fallat",
     "ScheduledTaskFailedWithName": "{0} ha fallat",
@@ -72,10 +72,10 @@
     "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
     "ServerNameNeedsToBeRestarted": "S'ha de reiniciar {0}",
     "Shows": "Sèries",
     "Shows": "Sèries",
     "Songs": "Cançons",
     "Songs": "Cançons",
-    "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu de nou en una estona.",
+    "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho de nou en una estona.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
     "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
-    "Sync": "Sincronitzar",
+    "Sync": "Sincronitza",
     "System": "Sistema",
     "System": "Sistema",
     "TvShows": "Sèries de TV",
     "TvShows": "Sèries de TV",
     "User": "Usuari",
     "User": "Usuari",
@@ -89,52 +89,54 @@
     "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
     "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
     "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
     "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1} a {2}",
     "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
     "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1} a {2}",
-    "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la teva biblioteca",
+    "ValueHasBeenAddedToLibrary": "S'ha afegit {0} a la mediateca",
     "ValueSpecialEpisodeName": "Especial - {0}",
     "ValueSpecialEpisodeName": "Especial - {0}",
     "VersionNumber": "Versió {0}",
     "VersionNumber": "Versió {0}",
     "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
     "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
-    "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
+    "TaskDownloadMissingSubtitles": "Descàrrega dels subtítols que faltin",
     "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
     "TaskRefreshChannelsDescription": "Actualitza la informació dels canals per internet.",
     "TaskRefreshChannels": "Actualitza els canals",
     "TaskRefreshChannels": "Actualitza els canals",
-    "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
-    "TaskCleanTranscode": "Neteja les transcodificacions",
-    "TaskUpdatePluginsDescription": "Actualitza els complements que estan configurats per a actualitzar-se automàticament.",
-    "TaskUpdatePlugins": "Actualitza els complements",
-    "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva biblioteca de mitjans.",
-    "TaskRefreshPeople": "Actualitza les persones",
-    "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
-    "TaskCleanLogs": "Neteja els registres",
-    "TaskRefreshLibraryDescription": "Escaneja la biblioteca de mitjans buscant fitxers nous i refresca les metadades.",
-    "TaskRefreshLibrary": "Escaneja la biblioteca de mitjans",
-    "TaskRefreshChapterImagesDescription": "Crea les miniatures dels vídeos que tinguin capítols.",
-    "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
-    "TaskCleanCacheDescription": "Elimina la memòria cau no necessària per al servidor.",
-    "TaskCleanCache": "Elimina la memòria cau",
+    "TaskCleanTranscodeDescription": "Elimina els fitxers de transcodificacions que tinguin més d'un dia.",
+    "TaskCleanTranscode": "Neteja de les transcodificacions",
+    "TaskUpdatePluginsDescription": "Descarrega i instal·la els complements que estiguin configurats per a actualitzar-se automàticament.",
+    "TaskUpdatePlugins": "Actualització dels complements",
+    "TaskRefreshPeopleDescription": "Actualització de les metadades dels actors i directors de la mediateca.",
+    "TaskRefreshPeople": "Actualització de les persones",
+    "TaskCleanLogsDescription": "Esborra els registres que tinguin més de {0} dies.",
+    "TaskCleanLogs": "Neteja dels registres",
+    "TaskRefreshLibraryDescription": "Escaneja les mediateques, a la cerca de fitxers nous i refresca les metadades.",
+    "TaskRefreshLibrary": "Escaneig de les mediateques",
+    "TaskRefreshChapterImagesDescription": "Creació de les miniatures dels vídeos que tinguin capítols.",
+    "TaskRefreshChapterImages": "Extracció de les imatges dels capítols",
+    "TaskCleanCacheDescription": "Eliminació de la memòria cau no necessària per al servidor.",
+    "TaskCleanCache": "Eliminació de la memòria cau",
     "TasksChannelsCategory": "Canals per internet",
     "TasksChannelsCategory": "Canals per internet",
     "TasksApplicationCategory": "Aplicatiu",
     "TasksApplicationCategory": "Aplicatiu",
-    "TasksLibraryCategory": "Biblioteca",
+    "TasksLibraryCategory": "Mediateca",
     "TasksMaintenanceCategory": "Manteniment",
     "TasksMaintenanceCategory": "Manteniment",
-    "TaskCleanActivityLogDescription": "Eliminades les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
-    "TaskCleanActivityLog": "Buidar el registre d'activitat",
+    "TaskCleanActivityLogDescription": "Eliminació de les entrades del registre d'activitats més antigues que l'antiguitat configurada.",
+    "TaskCleanActivityLog": "Buidatge del registre d'activitat",
     "Undefined": "Indefinit",
     "Undefined": "Indefinit",
     "Forced": "Forçat",
     "Forced": "Forçat",
     "Default": "Per defecte",
     "Default": "Per defecte",
-    "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la biblioteca o fer altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
-    "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",
+    "TaskOptimizeDatabaseDescription": "Compacta la base de dades i trunca l'espai lliure. Executar aquesta tasca després d’escanejar la mediateca o fer d'altres canvis que impliquin modificacions a la base de dades pot millorar el rendiment.",
+    "TaskOptimizeDatabase": "Optimització de la base de dades",
+    "TaskKeyframeExtractorDescription": "Extracció de fotogrames clau dels fitxers de vídeo per a crear llistes de reproducció HLS més precises. Aquesta tasca pot allargar-se molt en el temps.",
+    "TaskKeyframeExtractor": "Extracció de fotogrames clau",
     "External": "Extern",
     "External": "Extern",
     "HearingImpaired": "Discapacitat auditiva",
     "HearingImpaired": "Discapacitat auditiva",
-    "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps",
-    "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades.",
+    "TaskRefreshTrickplayImages": "Generació d'imatges de previsualització",
+    "TaskRefreshTrickplayImagesDescription": "Creació d'imatges de previsualització per a vídeos en les mediateques habilitades.",
     "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
     "TaskCleanCollectionsAndPlaylistsDescription": "Esborra elements de col·leccions i llistes de reproducció que ja no existeixen.",
-    "TaskCleanCollectionsAndPlaylists": "Neteja les col·leccions i llistes de reproducció",
-    "TaskAudioNormalization": "Estabilització dudio",
-    "TaskAudioNormalizationDescription": "Escaneja arxius per dades d'estabilització d'àudio.",
-    "TaskDownloadMissingLyricsDescription": "Baixar les lletres de les cançons",
-    "TaskDownloadMissingLyrics": "Baixar les lletres que falten",
+    "TaskCleanCollectionsAndPlaylists": "Neteja de les col·leccions i llistes de reproducció",
+    "TaskAudioNormalization": "Estabilització de l'àudio",
+    "TaskAudioNormalizationDescription": "Escaneja els fitxer per a obtenir dades de normalització de l'àudio.",
+    "TaskDownloadMissingLyricsDescription": "Descàrrega de les lletres de les cançons",
+    "TaskDownloadMissingLyrics": "Descàrrega de les lletres que faltin",
     "TaskExtractMediaSegments": "Escaneig de segments multimèdia",
     "TaskExtractMediaSegments": "Escaneig de segments multimèdia",
     "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
     "TaskExtractMediaSegmentsDescription": "Extreu o obté segments multimèdia usant els connectors MediaSegment activats.",
-    "TaskMoveTrickplayImages": "Migra la ubicació de la imatge de Trickplay",
-    "TaskMoveTrickplayImagesDescription": "Mou els fitxers trickplay existents segons la configuració de la biblioteca."
+    "TaskMoveTrickplayImages": "Migració de la ubicació de la imatge de previsualització",
+    "TaskMoveTrickplayImagesDescription": "Mou els fitxers existents d'imatges de previsualització segons la configuració de la mediateca.",
+    "CleanupUserDataTaskDescription": "Neteja totes les dades d'usuari (estat de la visualització, estat dels preferits, etc.) del contingut multimèdia que no ha estat present durant almenys 90 dies.",
+    "CleanupUserDataTask": "Tasca de neteja de dades d'usuari"
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskExtractMediaSegments": "Skenování segmentů médií",
     "TaskExtractMediaSegments": "Skenování segmentů médií",
     "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
     "TaskExtractMediaSegmentsDescription": "Extrahuje či získá segmenty médií pomocí zásuvných modulů MediaSegment.",
     "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
     "TaskMoveTrickplayImages": "Přesunout úložiště obrázků Trickplay",
-    "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny."
+    "TaskMoveTrickplayImagesDescription": "Přesune existující soubory Trickplay podle nastavení knihovny.",
+    "CleanupUserDataTaskDescription": "Odstraní všechna uživatelská data (stav zhlédnutí, oblíbené atd.) z médií, které již neexistují více než 90 dní.",
+    "CleanupUserDataTask": "Pročistit uživatelská data"
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskExtractMediaSegments": "Scan for mediesegmenter",
     "TaskExtractMediaSegments": "Scan for mediesegmenter",
     "TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
     "TaskMoveTrickplayImages": "Migrer billedelokationer for trickplay-billeder",
     "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
     "TaskMoveTrickplayImagesDescription": "Flyt eksisterende trickplay-billeder jævnfør biblioteksindstillinger.",
-    "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment."
+    "TaskExtractMediaSegmentsDescription": "Udtrækker eller henter mediesegmenter fra plugins som understøtter MediaSegment.",
+    "CleanupUserDataTask": "Brugerdata oprydningsopgave",
+    "CleanupUserDataTaskDescription": "Rydder alle brugerdata (eks. visning- og favoritstatus) fra medier, der har været utilgængelige i mindst 90 dage."
 }
 }

+ 24 - 22
Emby.Server.Implementations/Localization/Core/de.json

@@ -43,7 +43,7 @@
     "NameInstallFailed": "Installation von {0} fehlgeschlagen",
     "NameInstallFailed": "Installation von {0} fehlgeschlagen",
     "NameSeasonNumber": "Staffel {0}",
     "NameSeasonNumber": "Staffel {0}",
     "NameSeasonUnknown": "Staffel unbekannt",
     "NameSeasonUnknown": "Staffel unbekannt",
-    "NewVersionIsAvailable": "Eine neue Version von Jellyfin-Server steht zum Download bereit.",
+    "NewVersionIsAvailable": "Eine neue Jellyfin-Serverversion steht zum Download bereit.",
     "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
     "NotificationOptionApplicationUpdateAvailable": "Anwendungsaktualisierung verfügbar",
     "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
     "NotificationOptionApplicationUpdateInstalled": "Anwendungsaktualisierung installiert",
     "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
     "NotificationOptionAudioPlayback": "Audiowiedergabe gestartet",
@@ -72,12 +72,12 @@
     "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
     "ServerNameNeedsToBeRestarted": "{0} muss neu gestartet werden",
     "Shows": "Serien",
     "Shows": "Serien",
     "Songs": "Lieder",
     "Songs": "Lieder",
-    "StartupEmbyServerIsLoading": "Jellyfin-Server startet, bitte versuche es gleich noch einmal.",
+    "StartupEmbyServerIsLoading": "Jellyfin-Server lädt. Bitte versuche es gleich noch einmal.",
     "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
     "SubtitleDownloadFailureForItem": "Download der Untertitel fehlgeschlagen für {0}",
     "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
     "SubtitleDownloadFailureFromForItem": "Untertitel von {0} für {1} konnten nicht heruntergeladen werden",
     "Sync": "Synchronisation",
     "Sync": "Synchronisation",
     "System": "System",
     "System": "System",
-    "TvShows": "TV-Sendungen",
+    "TvShows": "Serien",
     "User": "Benutzer",
     "User": "Benutzer",
     "UserCreatedWithName": "Benutzer {0} wurde erstellt",
     "UserCreatedWithName": "Benutzer {0} wurde erstellt",
     "UserDeletedWithName": "Benutzer {0} wurde gelöscht",
     "UserDeletedWithName": "Benutzer {0} wurde gelöscht",
@@ -90,32 +90,32 @@
     "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
     "UserStartedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} gestartet",
     "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
     "UserStoppedPlayingItemWithValues": "{0} hat die Wiedergabe von {1} auf {2} beendet",
     "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
     "ValueHasBeenAddedToLibrary": "{0} wurde deiner Bibliothek hinzugefügt",
-    "ValueSpecialEpisodeName": "Extra - {0}",
+    "ValueSpecialEpisodeName": "Extra  {0}",
     "VersionNumber": "Version {0}",
     "VersionNumber": "Version {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Suche im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
-    "TaskDownloadMissingSubtitles": "Lade fehlende Untertitel herunter",
-    "TaskRefreshChannelsDescription": "Aktualisiere Internet-Kanal-Informationen.",
-    "TaskRefreshChannels": "Aktualisiere Kanäle",
-    "TaskCleanTranscodeDescription": "Löscht Transkodierdateien, die älter als einen Tag sind.",
-    "TaskCleanTranscode": "Räume Transkodierungs-Verzeichnis auf",
+    "TaskDownloadMissingSubtitlesDescription": "Sucht im Internet basierend auf den Metadaten-Einstellungen nach fehlenden Untertiteln.",
+    "TaskDownloadMissingSubtitles": "Fehlende Untertitel herunterladen",
+    "TaskRefreshChannelsDescription": "Aktualisiert Internet-Kanal-Informationen.",
+    "TaskRefreshChannels": "Kanäle aktualisieren",
+    "TaskCleanTranscodeDescription": "Löscht Transkodierungsdateien, die älter als einen Tag sind.",
+    "TaskCleanTranscode": "Transkodierungs-Verzeichnis aufräumen",
     "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
     "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche für automatische Updates konfiguriert sind und installiert diese.",
-    "TaskUpdatePlugins": "Aktualisiere Plugins",
+    "TaskUpdatePlugins": "Plugins aktualisieren",
     "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
     "TaskRefreshPeopleDescription": "Aktualisiert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
-    "TaskRefreshPeople": "Aktualisiere Personen",
+    "TaskRefreshPeople": "Personen aktualisieren",
     "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
     "TaskCleanLogsDescription": "Lösche Log-Dateien, die älter als {0} Tage sind.",
-    "TaskCleanLogs": "Räumt Log-Verzeichnis auf",
-    "TaskRefreshLibraryDescription": "Scannt alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiere Metadaten.",
-    "TaskRefreshLibrary": "Scanne Medien-Bibliothek",
+    "TaskCleanLogs": "Log-Verzeichnis aufräumen",
+    "TaskRefreshLibraryDescription": "Durchsucht alle Bibliotheken nach neu hinzugefügten Dateien und aktualisiert Metadaten.",
+    "TaskRefreshLibrary": "Medien-Bibliothek scannen",
     "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
     "TaskRefreshChapterImagesDescription": "Erstellt Vorschaubilder für Videos, die Kapitel besitzen.",
-    "TaskRefreshChapterImages": "Extrahiere Kapitel-Bilder",
-    "TaskCleanCacheDescription": "Löscht nicht mehr benötigte Zwischenspeicherdateien.",
-    "TaskCleanCache": "Leere Zwischenspeicher",
+    "TaskRefreshChapterImages": "Kapitel-Bilder extrahieren",
+    "TaskCleanCacheDescription": "Löscht vom System nicht mehr benötigte Zwischenspeicherdateien.",
+    "TaskCleanCache": "Zwischenspeicher-Verzeichnis aufräumen",
     "TasksChannelsCategory": "Internet-Kanäle",
     "TasksChannelsCategory": "Internet-Kanäle",
     "TasksApplicationCategory": "Anwendung",
     "TasksApplicationCategory": "Anwendung",
     "TasksLibraryCategory": "Bibliothek",
     "TasksLibraryCategory": "Bibliothek",
     "TasksMaintenanceCategory": "Wartung",
     "TasksMaintenanceCategory": "Wartung",
     "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
     "TaskCleanActivityLogDescription": "Löscht Aktivitätsprotokolleinträge, die älter als das konfigurierte Alter sind.",
-    "TaskCleanActivityLog": "Aktivitätsprotokoll aufräumen",
+    "TaskCleanActivityLog": "Aktivitätsprotokolle aufräumen",
     "Undefined": "Undefiniert",
     "Undefined": "Undefiniert",
     "Forced": "Erzwungen",
     "Forced": "Erzwungen",
     "Default": "Standard",
     "Default": "Standard",
@@ -128,13 +128,15 @@
     "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
     "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
     "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
     "TaskRefreshTrickplayImagesDescription": "Erstellt ein Trickplay-Vorschauen für Videos in aktivierten Bibliotheken.",
     "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
     "TaskCleanCollectionsAndPlaylists": "Sammlungen und Playlisten aufräumen",
-    "TaskCleanCollectionsAndPlaylistsDescription": "Lösche nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
+    "TaskCleanCollectionsAndPlaylistsDescription": "Löscht nicht mehr vorhandene Einträge aus den Sammlungen und Playlisten.",
     "TaskAudioNormalization": "Audio Normalisierung",
     "TaskAudioNormalization": "Audio Normalisierung",
     "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
     "TaskAudioNormalizationDescription": "Durchsucht Dateien nach Audionormalisierungsdaten.",
     "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
     "TaskDownloadMissingLyricsDescription": "Lädt Songtexte herunter",
     "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen",
     "TaskDownloadMissingLyrics": "Fehlende Songtexte herunterladen",
-    "TaskExtractMediaSegments": "Scanne Mediensegmente",
+    "TaskExtractMediaSegments": "Mediensegmente scannen",
     "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
     "TaskExtractMediaSegmentsDescription": "Extrahiert oder empfängt Mediensegmente von Plugins die Mediensegmente nutzen.",
     "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
     "TaskMoveTrickplayImages": "Verzeichnis für Trickplay-Bilder migrieren",
-    "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben."
+    "TaskMoveTrickplayImagesDescription": "Trickplay-Bilder werden entsprechend der Bibliothekseinstellungen verschoben.",
+    "CleanupUserDataTask": "Aufgabe zur Bereinigung von Benutzerdaten",
+    "CleanupUserDataTaskDescription": "Löscht alle Benutzerdaten (Anschaustatus, Favoritenstatus, usw.) von Medien, die seit mindestens 90 Tagen nicht mehr vorhanden sind."
 }
 }

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

@@ -11,7 +11,7 @@
     "Collections": "Συλλογές",
     "Collections": "Συλλογές",
     "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
     "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε",
     "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
     "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε",
-    "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}",
+    "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}",
     "Favorites": "Αγαπημένα",
     "Favorites": "Αγαπημένα",
     "Folders": "Φάκελοι",
     "Folders": "Φάκελοι",
     "Genres": "Είδη",
     "Genres": "Είδη",
@@ -27,8 +27,8 @@
     "HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
     "HeaderRecordingGroups": "Ομάδες Ηχογράφησης",
     "HomeVideos": "Προσωπικά Βίντεο",
     "HomeVideos": "Προσωπικά Βίντεο",
     "Inherit": "Κληρονόμηση",
     "Inherit": "Κληρονόμηση",
-    "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη",
-    "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη",
+    "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη",
+    "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη",
     "LabelIpAddressValue": "Διεύθυνση IP: {0}",
     "LabelIpAddressValue": "Διεύθυνση IP: {0}",
     "LabelRunningTimeValue": "Διάρκεια: {0}",
     "LabelRunningTimeValue": "Διάρκεια: {0}",
     "Latest": "Πρόσφατα",
     "Latest": "Πρόσφατα",
@@ -40,7 +40,7 @@
     "Movies": "Ταινίες",
     "Movies": "Ταινίες",
     "Music": "Μουσική",
     "Music": "Μουσική",
     "MusicVideos": "Μουσικά Βίντεο",
     "MusicVideos": "Μουσικά Βίντεο",
-    "NameInstallFailed": "{0} η εγκατάσταση απέτυχε",
+    "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε",
     "NameSeasonNumber": "Κύκλος {0}",
     "NameSeasonNumber": "Κύκλος {0}",
     "NameSeasonUnknown": "Άγνωστος Κύκλος",
     "NameSeasonUnknown": "Άγνωστος Κύκλος",
     "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
     "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.",
@@ -54,7 +54,7 @@
     "NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
     "NotificationOptionPluginError": "Αποτυχία του πρόσθετου",
     "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
     "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε",
     "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
     "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε",
-    "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε",
+    "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε",
     "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
     "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση",
     "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
     "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας",
     "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
     "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε",
@@ -63,9 +63,9 @@
     "Photos": "Φωτογραφίες",
     "Photos": "Φωτογραφίες",
     "Playlists": "Λίστες αναπαραγωγής",
     "Playlists": "Λίστες αναπαραγωγής",
     "Plugin": "Πρόσθετο",
     "Plugin": "Πρόσθετο",
-    "PluginInstalledWithName": "{0} εγκαταστήθηκε",
-    "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί",
-    "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί",
+    "PluginInstalledWithName": "Το {0} εγκαταστάθηκε",
+    "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί",
+    "PluginUpdatedWithName": "Το {0} ενημερώθηκε",
     "ProviderValue": "Πάροχος: {0}",
     "ProviderValue": "Πάροχος: {0}",
     "ScheduledTaskFailedWithName": "{0} αποτυχία",
     "ScheduledTaskFailedWithName": "{0} αποτυχία",
     "ScheduledTaskStartedWithName": "{0} ξεκίνησε",
     "ScheduledTaskStartedWithName": "{0} ξεκίνησε",
@@ -96,7 +96,7 @@
     "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
     "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.",
     "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
     "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής",
     "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
     "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.",
-    "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων",
+    "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων",
     "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
     "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.",
     "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
     "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου",
     "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
     "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.",
@@ -125,7 +125,7 @@
     "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
     "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
     "External": "Εξωτερικό",
     "External": "Εξωτερικό",
     "HearingImpaired": "Με προβλήματα ακοής",
     "HearingImpaired": "Με προβλήματα ακοής",
-    "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+    "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay",
     "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
     "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.",
     "TaskAudioNormalization": "Ομοιομορφία ήχου",
     "TaskAudioNormalization": "Ομοιομορφία ήχου",
     "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",
     "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.",

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

@@ -136,5 +136,7 @@
     "TaskExtractMediaSegments": "Media Segment Scan",
     "TaskExtractMediaSegments": "Media Segment Scan",
     "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
     "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
     "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
     "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
-    "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
+    "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
+    "CleanupUserDataTask": "User data cleanup task",
+    "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favourite status etc) from media that is no longer present for at least 90 days."
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskExtractMediaSegments": "Media Segment Scan",
     "TaskExtractMediaSegments": "Media Segment Scan",
     "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
     "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.",
     "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
     "TaskMoveTrickplayImages": "Migrate Trickplay Image Location",
-    "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings."
+    "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings.",
+    "CleanupUserDataTask": "User data cleanup task",
+    "CleanupUserDataTaskDescription": "Cleans all user data (Watch state, favorite status etc) from media that is no longer present for at least 90 days."
 }
 }

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

@@ -122,5 +122,9 @@
     "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
     "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis",
     "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
     "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.",
     "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
     "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn",
-    "External": "Ekstera"
+    "External": "Ekstera",
+    "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.",
+    "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)",
+    "TaskAudioNormalization": "Normaligo Sonnivela",
+    "HearingImpaired": "Surda"
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
     "TaskMoveTrickplayImagesDescription": "Mueve archivos de trickplay existentes según la configuración de la biblioteca.",
     "TaskExtractMediaSegments": "Escaneo de segmentos de medios",
     "TaskExtractMediaSegments": "Escaneo de segmentos de medios",
     "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
     "TaskExtractMediaSegmentsDescription": "Extrae u obtiene segmentos de medios de plugins habilitados para MediaSegment.",
-    "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay"
+    "TaskMoveTrickplayImages": "Migrar la ubicación de la imagen de Trickplay",
+    "CleanupUserDataTask": "Tarea de limpieza de datos del usuario",
+    "CleanupUserDataTaskDescription": "Limpia todos los datos del usuario (estado de visualización, favoritos, etc.) de los medios que ya no están disponibles desde hace al menos 90 días."
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
     "TaskExtractMediaSegmentsDescription": "Media segmentuak atera edo lortzen ditu MediaSegment gaituta duten pluginetik.",
     "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
     "TaskMoveTrickplayImages": "Aldatu Trickplay irudien kokalekua",
     "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
     "TaskMoveTrickplayImagesDescription": "Lehendik dauden trickplay fitxategiak liburutegiaren ezarpenen arabera mugitzen dira.",
-    "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu."
+    "TaskAudioNormalizationDescription": "Audio normalizazio datuak lortzeko fitxategiak eskaneatzen ditu.",
+    "CleanupUserDataTaskDescription": "Gutxienez 90 egunez dagoeneko existitzen ez den multimediatik erabiltzaile-datu guztiak (ikusteko egoera, gogokoen egoera, etab.) garbitzen ditu.",
+    "CleanupUserDataTask": "Erabiltzaileen datuak garbitzeko zeregina"
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
     "TaskDownloadMissingLyricsDescription": "Ladataan sanoituksia",
     "TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
     "TaskExtractMediaSegmentsDescription": "Poimii tai hankkii mediasegmenttejä MediaSegment-yhteensopivista laajennuksista.",
     "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
     "TaskMoveTrickplayImages": "Siirrä Trickplay-kuvien sijainti",
-    "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan."
+    "TaskMoveTrickplayImagesDescription": "Siirtää olemassa olevia trickplay-tiedostoja kirjaston asetusten mukaan.",
+    "CleanupUserDataTask": "Käyttäjätietojen puhdistustehtävä",
+    "CleanupUserDataTaskDescription": "Puhdistaa kaikki käyttäjätiedot (katselutila, suosikit ym.) medioista, joita ei ole ollut saatavilla yli 90 päivään."
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
     "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
     "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
     "TaskDownloadMissingLyrics": "Télécharger les paroles des chansons manquantes",
     "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
     "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
-    "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment."
+    "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
+    "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
+    "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
 }
 }

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

@@ -136,5 +136,7 @@
     "TaskExtractMediaSegments": "Analyse des segments de média",
     "TaskExtractMediaSegments": "Analyse des segments de média",
     "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
     "TaskMoveTrickplayImages": "Changer l'emplacement des images Trickplay",
     "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
     "TaskExtractMediaSegmentsDescription": "Extrait ou obtient des segments de média à partir des plugins compatibles avec MediaSegment.",
-    "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque."
+    "TaskMoveTrickplayImagesDescription": "Déplace les fichiers trickplay existants en fonction des paramètres de la bibliothèque.",
+    "CleanupUserDataTaskDescription": "Nettoie toutes les données utilisateur (état de la montre, statut favori, etc.) des supports qui ne sont plus présents depuis au moins 90 jours.",
+    "CleanupUserDataTask": "Tâche de nettoyage des données utilisateur"
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskUpdatePlugins": "Nuashonraigh Breiseáin",
     "TaskUpdatePlugins": "Nuashonraigh Breiseáin",
     "TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
     "TaskCleanTranscodeDescription": "Scriostar comhaid traschódaithe níos mó ná lá amháin d'aois.",
     "TaskCleanTranscode": "Eolaire Transcode Glan",
     "TaskCleanTranscode": "Eolaire Transcode Glan",
-    "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh"
+    "TaskDownloadMissingSubtitles": "Íosluchtaigh fotheidil ar iarraidh",
+    "CleanupUserDataTask": "Tasc glantacháin sonraí úsáideora",
+    "CleanupUserDataTaskDescription": "Glanann sé gach sonraí úsáideora (stádas faire, stádas is fearr leat srl.) ó mheáin nach bhfuil i láthair a thuilleadh ar feadh 90 lá ar a laghad."
 }
 }

+ 14 - 1
Emby.Server.Implementations/Localization/Core/gl.json

@@ -123,5 +123,18 @@
     "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
     "TaskKeyframeExtractorDescription": "Extrae fragmentos do vídeo para crear listas de reprodución HLS máis precisas. Podería levarlle bastante tempo.",
     "External": "Externo",
     "External": "Externo",
     "HearingImpaired": "Problemas de audición",
     "HearingImpaired": "Problemas de audición",
-    "TaskKeyframeExtractor": "Extractor de fragmentos"
+    "TaskKeyframeExtractor": "Extractor de fragmentos",
+    "TaskAudioNormalization": "Normalización do audio",
+    "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reprodución con truco para vídeos en bibliotecas activadas.",
+    "TaskDownloadMissingLyrics": "Descargar letras que faltan",
+    "TaskDownloadMissingLyricsDescription": "Descargas de letras das cancións",
+    "TaskCleanCollectionsAndPlaylists": "Limpar coleccións e listas de reprodución",
+    "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de coleccións e listas de reprodución que xa non existen.",
+    "TaskExtractMediaSegmentsDescription": "Extrae ou obtén segmentos multimedia de complementos habilitados para o Segmento de medios.",
+    "TaskExtractMediaSegments": "Escaneo de segmentos multimedia",
+    "TaskMoveTrickplayImages": "Migrar a localización da imaxe de Trickplay",
+    "TaskMoveTrickplayImagesDescription": "Move os ficheiros de reprodución con trickplay existentes segundo a configuración da biblioteca.",
+    "TaskRefreshTrickplayImages": "Xerar imaxes de Trickplay",
+    "TaskAudioNormalizationDescription": "Analiza ficheiros para obter datos de normalización de audio.",
+    "CleanupUserDataTask": "Tarefa de limpeza de datos do usuario"
 }
 }

+ 13 - 11
Emby.Server.Implementations/Localization/Core/he.json

@@ -32,8 +32,8 @@
     "LabelIpAddressValue": "Ip כתובת: {0}",
     "LabelIpAddressValue": "Ip כתובת: {0}",
     "LabelRunningTimeValue": "משך צפייה: {0}",
     "LabelRunningTimeValue": "משך צפייה: {0}",
     "Latest": "אחרון",
     "Latest": "אחרון",
-    "MessageApplicationUpdated": "שרת ג'ליפין עודכן",
-    "MessageApplicationUpdatedTo": "שרת ג'ליפין עודכן לגרסה {0}",
+    "MessageApplicationUpdated": "שרת Jellyfin עודכן",
+    "MessageApplicationUpdatedTo": "שרת Jellyfin עודכן לגרסה {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
     "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
     "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
     "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
     "MixedContent": "תוכן מעורב",
     "MixedContent": "תוכן מעורב",
@@ -43,7 +43,7 @@
     "NameInstallFailed": "התקנת {0} נכשלה",
     "NameInstallFailed": "התקנת {0} נכשלה",
     "NameSeasonNumber": "עונה {0}",
     "NameSeasonNumber": "עונה {0}",
     "NameSeasonUnknown": "עונה לא ידועה",
     "NameSeasonUnknown": "עונה לא ידועה",
-    "NewVersionIsAvailable": "גרסה חדשה של שרת ג'ליפין זמינה להורדה.",
+    "NewVersionIsAvailable": "גרסה חדשה של שרת Jellyfin זמינה להורדה.",
     "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
     "NotificationOptionApplicationUpdateAvailable": "קיים עדכון זמין ליישום",
     "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
     "NotificationOptionApplicationUpdateInstalled": "עדכון ליישום הותקן",
     "NotificationOptionAudioPlayback": "ניגון שמע החל",
     "NotificationOptionAudioPlayback": "ניגון שמע החל",
@@ -72,7 +72,7 @@
     "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
     "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
     "Shows": "סדרות",
     "Shows": "סדרות",
     "Songs": "שירים",
     "Songs": "שירים",
-    "StartupEmbyServerIsLoading": "שרת ג'ליפין טוען. נא לנסות שוב בקרוב.",
+    "StartupEmbyServerIsLoading": "שרת Jellyfin בתהליך טעינה. נא לנסות שוב בקרוב.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
     "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
     "Sync": "סנכרון",
     "Sync": "סנכרון",
@@ -100,14 +100,14 @@
     "TasksLibraryCategory": "ספרייה",
     "TasksLibraryCategory": "ספרייה",
     "TasksMaintenanceCategory": "תחזוקה",
     "TasksMaintenanceCategory": "תחזוקה",
     "TaskUpdatePlugins": "עדכן תוספים",
     "TaskUpdatePlugins": "עדכן תוספים",
-    "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
+    "TaskRefreshPeopleDescription": "מעדכן מטא-דאטה עבור שחקנים ובמאים בספריית המדיה שלך.",
     "TaskRefreshPeople": "רענן אנשים",
     "TaskRefreshPeople": "רענן אנשים",
     "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
     "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
     "TaskCleanLogs": "ניקוי תיקיית יומן",
     "TaskCleanLogs": "ניקוי תיקיית יומן",
-    "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
+    "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא-דאטה.",
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TasksChannelsCategory": "ערוצי אינטרנט",
     "TasksChannelsCategory": "ערוצי אינטרנט",
-    "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט עבור הכתוביות החסרות בהתבסס על המטה-דיאטה.",
+    "TaskDownloadMissingSubtitlesDescription": "חפש באינטרנט כתוביות חסרות בהתבסס על המטא-דאטה.",
     "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
     "TaskDownloadMissingSubtitles": "הורד כתוביות חסרות",
     "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
     "TaskRefreshChannelsDescription": "רענן פרטי ערוץ אינטרנטי.",
     "TaskRefreshChannels": "רענן ערוץ",
     "TaskRefreshChannels": "רענן ערוץ",
@@ -125,16 +125,18 @@
     "TaskKeyframeExtractor": "מחלץ תמונות מפתח",
     "TaskKeyframeExtractor": "מחלץ תמונות מפתח",
     "External": "חיצוני",
     "External": "חיצוני",
     "HearingImpaired": "לקוי שמיעה",
     "HearingImpaired": "לקוי שמיעה",
-    "TaskRefreshTrickplayImages": "יצירת תמונות המחשה",
-    "TaskRefreshTrickplayImagesDescription": "יוצר תמונות המחשה לסרטונים שפעילים בספריות.",
+    "TaskRefreshTrickplayImages": "יצירת תמונות Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "יוצר תמונות Trickplay לסרטונים בספריות הפעילות.",
     "TaskAudioNormalization": "נרמול שמע",
     "TaskAudioNormalization": "נרמול שמע",
     "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
     "TaskCleanCollectionsAndPlaylistsDescription": "מנקה פריטים לא קיימים מאוספים ורשימות השמעה.",
     "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
     "TaskAudioNormalizationDescription": "מחפש קבצי נורמליזציה של שמע.",
     "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
     "TaskCleanCollectionsAndPlaylists": "מנקה אוספים ורשימות השמעה",
     "TaskDownloadMissingLyrics": "הורדת מילים חסרות",
     "TaskDownloadMissingLyrics": "הורדת מילים חסרות",
     "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
     "TaskDownloadMissingLyricsDescription": "הורדת מילים לשירים",
-    "TaskMoveTrickplayImages": "העברת מיקום התמונות",
+    "TaskMoveTrickplayImages": "העברת מיקום של תמונות Trickplay",
     "TaskExtractMediaSegments": "סריקת מדיה",
     "TaskExtractMediaSegments": "סריקת מדיה",
     "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
     "TaskExtractMediaSegmentsDescription": "מחלץ חלקי מדיה מתוספים המאפשרים זאת.",
-    "TaskMoveTrickplayImagesDescription": "הזזת קבצי טריקפליי קיימים בהתאם להגדרות הספרייה."
+    "TaskMoveTrickplayImagesDescription": "הזזת קבצי Trickplay קיימים בהתאם להגדרות הספרייה.",
+    "CleanupUserDataTaskDescription": "ניקוי כל המידע של המשתמש (מצב צפייה, מועדפים וכו) ממדיה שאינה קיימת מעל 90 יום.",
+    "CleanupUserDataTask": "משימת ניקוי מידע משתמש"
 }
 }

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

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

+ 5 - 3
Emby.Server.Implementations/Localization/Core/hu.json

@@ -1,6 +1,6 @@
 {
 {
     "Albums": "Albumok",
     "Albums": "Albumok",
-    "AppDeviceValues": "Program: {0}, eszköz: {1}",
+    "AppDeviceValues": "Program: {0}, Eszköz: {1}",
     "Application": "Alkalmazás",
     "Application": "Alkalmazás",
     "Artists": "Előadók",
     "Artists": "Előadók",
     "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
     "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
@@ -13,7 +13,7 @@
     "DeviceOnlineWithName": "{0} belépett",
     "DeviceOnlineWithName": "{0} belépett",
     "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
     "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
     "Favorites": "Kedvencek",
     "Favorites": "Kedvencek",
-    "Folders": "Könyvtárak",
+    "Folders": "Mappák",
     "Genres": "Műfajok",
     "Genres": "Műfajok",
     "HeaderAlbumArtists": "Albumelőadók",
     "HeaderAlbumArtists": "Albumelőadók",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderContinueWatching": "Megtekintés folytatása",
@@ -136,5 +136,7 @@
     "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
     "TaskDownloadMissingLyricsDescription": "Zenék szövegének letöltése",
     "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
     "TaskMoveTrickplayImages": "Trickplay képek helyének átköltöztetése",
     "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
     "TaskMoveTrickplayImagesDescription": "A médiatár-beállításoknak megfelelően áthelyezi a meglévő trickplay fájlokat.",
-    "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből."
+    "TaskExtractMediaSegmentsDescription": "Kinyeri vagy megszerzi a médiaszegmenseket a MediaSegment támogatással rendelkező bővítményekből.",
+    "CleanupUserDataTaskDescription": "Legalább 90 napja nem elérhető médiákhoz kapcsolódó összes felhasználói adat (pl. megtekintési állapot, kedvencek) törlése.",
+    "CleanupUserDataTask": "Felhasználói adatok tisztítása feladat"
 }
 }

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

@@ -129,5 +129,13 @@
     "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
     "TaskAudioNormalizationDescription": "Pindai file untuk data normalisasi audio.",
     "TaskAudioNormalization": "Normalisasi Audio",
     "TaskAudioNormalization": "Normalisasi Audio",
     "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
     "TaskCleanCollectionsAndPlaylists": "Bersihkan koleksi dan daftar putar",
-    "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada."
+    "TaskCleanCollectionsAndPlaylistsDescription": "Menghapus item dari koleksi dan daftar putar yang sudah tidak ada.",
+    "TaskDownloadMissingLyricsDescription": "Unduh lirik untuk lagu",
+    "TaskExtractMediaSegmentsDescription": "Mengekstrak atau memperoleh segmen media dari plugin yang mendukung MediaSegment.",
+    "TaskMoveTrickplayImagesDescription": "Memindahkan file trickplay yang sudah ada sesuai dengan pengaturan pustaka.",
+    "CleanupUserDataTaskDescription": "Membersihkan semua data pengguna (status tontonan, status favorit, dll.) dari media yang sudah tidak ada selama setidaknya 90 hari.",
+    "TaskExtractMediaSegments": "Scan Segmen media",
+    "TaskMoveTrickplayImages": "Migrasikan Lokasi Gambar Trickplay",
+    "TaskDownloadMissingLyrics": "Unduh Lirik yang Hilang",
+    "CleanupUserDataTask": "Tugas Pembersihan Data Pengguna"
 }
 }

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

@@ -131,5 +131,8 @@
     "TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
     "TaskCleanCollectionsAndPlaylists": "Hreinsa söfn og spilunarlista",
     "TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
     "TaskCleanCollectionsAndPlaylistsDescription": "Fjarlægir hluti úr söfnum og spilalistum sem eru ekki lengur til.",
     "TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
     "TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
-    "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar"
+    "TaskDownloadMissingLyrics": "Sækja söngtexta sem vantar",
+    "TaskExtractMediaSegments": "Skönnun efnishluta",
+    "CleanupUserDataTask": "Hreinsun notendagagna",
+    "CleanupUserDataTaskDescription": "Hreinsar öll notendagögn (spilunarstöðu, uppáhöld o.s.frv.) um gögn sem hafa ekki verið til staðar í að lámarki 90 daga."
 }
 }

+ 5 - 3
Emby.Server.Implementations/Localization/Core/it.json

@@ -58,8 +58,8 @@
     "NotificationOptionServerRestartRequired": "Riavvio del server necessario",
     "NotificationOptionServerRestartRequired": "Riavvio del server necessario",
     "NotificationOptionTaskFailed": "Operazione pianificata fallita",
     "NotificationOptionTaskFailed": "Operazione pianificata fallita",
     "NotificationOptionUserLockedOut": "Utente bloccato",
     "NotificationOptionUserLockedOut": "Utente bloccato",
-    "NotificationOptionVideoPlayback": "La riproduzione video è iniziata",
-    "NotificationOptionVideoPlaybackStopped": "La riproduzione video è stata interrotta",
+    "NotificationOptionVideoPlayback": "Riproduzione video iniziata",
+    "NotificationOptionVideoPlaybackStopped": "Riproduzione video interrotta",
     "Photos": "Foto",
     "Photos": "Foto",
     "Playlists": "Playlist",
     "Playlists": "Playlist",
     "Plugin": "Plugin",
     "Plugin": "Plugin",
@@ -136,5 +136,7 @@
     "TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
     "TaskMoveTrickplayImages": "Sposta le immagini Trickplay",
     "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
     "TaskMoveTrickplayImagesDescription": "Sposta le immagini Trickplay esistenti secondo la configurazione della libreria.",
     "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
     "TaskExtractMediaSegmentsDescription": "Estrae o ottiene segmenti multimediali dai plugin abilitati MediaSegment.",
-    "TaskExtractMediaSegments": "Scansiona Segmento Media"
+    "TaskExtractMediaSegments": "Scansiona Segmento Media",
+    "CleanupUserDataTask": "Task di pulizia dei dati utente",
+    "CleanupUserDataTaskDescription": "Pulisce tutti i dati utente (stato di visione, status preferiti, ecc.) dai contenuti non più presenti da almeno 90 giorni."
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskMoveTrickplayImages": "Trickplayの画像を移動",
     "TaskMoveTrickplayImages": "Trickplayの画像を移動",
     "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
     "TaskMoveTrickplayImagesDescription": "ライブラリ設定によりTrickplayのファイルを移動。",
     "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
     "TaskDownloadMissingLyrics": "失われた歌詞をダウンロード",
-    "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。"
+    "TaskExtractMediaSegmentsDescription": "MediaSegment 対応プラグインからメディア セグメントを抽出または取得します。",
+    "CleanupUserDataTask": "ユーザーデータのクリーンアップタスク",
+    "CleanupUserDataTaskDescription": "90日以上存在しないメディアに対して、視聴状態やお気に入り状態などのユーザーデータをすべて削除します。"
 }
 }

+ 10 - 2
Emby.Server.Implementations/Localization/Core/kn.json

@@ -25,7 +25,7 @@
     "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
     "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
     "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
     "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
     "External": "ಹೊರಗಿನ",
     "External": "ಹೊರಗಿನ",
-    "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+    "FailedLoginAttemptWithUserName": "ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ ಸಂಖ್ಯೆ {0}",
     "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
     "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
     "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
     "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
     "Forced": "ಬಲವಂತವಾಗಿ",
     "Forced": "ಬಲವಂತವಾಗಿ",
@@ -123,5 +123,13 @@
     "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
     "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
     "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
     "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
     "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
     "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
-    "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
+    "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+    "TaskAudioNormalizationDescription": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ ಮಾಹಿತಿಗಾಗಿ ಕಡತ‌ಗಳನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ.",
+    "TaskDownloadMissingLyricsDescription": "ಹಾಡುಗಳಿಗೆ ಸಾಹಿತ್ಯ ಪಡೆಯಿರಿ",
+    "TaskExtractMediaSegments": "ಮಾಧ್ಯಮ ವಿಭಾಗದ ಹುಡುಕು",
+    "TaskDownloadMissingLyrics": "ಇಲ್ಲದ ಸಾಹಿತ್ಯವನ್ನು ಪಡೆಯಿರಿ",
+    "TaskAudioNormalization": "ಧ್ವನಿ ಸಾಮಾನ್ಯೀಕರಣ",
+    "TaskRefreshTrickplayImages": "ಟ್ರಿಕ್‌ಪ್ಲೇ ಚಿತ್ರಗಳನ್ನು ರಚಿಸಿ",
+    "TaskCleanCollectionsAndPlaylists": "ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+    "TaskCleanCollectionsAndPlaylistsDescription": "ಇಲ್ಲದ ಸಂಗ್ರಹಗಳು ಮತ್ತು ಪ್ಲೇಪಟ್ಟಿಗಳಿಂದ ವಸ್ತುಗಳನ್ನು ತೆಗೆದುಹಾಕುತ್ತದೆ."
 }
 }

+ 47 - 45
Emby.Server.Implementations/Localization/Core/lt-LT.json

@@ -8,7 +8,7 @@
     "CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
     "CameraImageUploadedFrom": "Nauja nuotrauka įkelta iš kameros {0}",
     "Channels": "Kanalai",
     "Channels": "Kanalai",
     "ChapterNameValue": "Scena{0}",
     "ChapterNameValue": "Scena{0}",
-    "Collections": "Kolekcijos",
+    "Collections": "Rinkiniai",
     "DeviceOfflineWithName": "{0} buvo atjungtas",
     "DeviceOfflineWithName": "{0} buvo atjungtas",
     "DeviceOnlineWithName": "{0} prisijungęs",
     "DeviceOnlineWithName": "{0} prisijungęs",
     "FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
     "FailedLoginAttemptWithUserName": "Nesėkmingas {0} bandymas prisijungti",
@@ -17,18 +17,18 @@
     "Genres": "Žanrai",
     "Genres": "Žanrai",
     "HeaderAlbumArtists": "Albumo atlikėjai",
     "HeaderAlbumArtists": "Albumo atlikėjai",
     "HeaderContinueWatching": "Žiūrėti toliau",
     "HeaderContinueWatching": "Žiūrėti toliau",
-    "HeaderFavoriteAlbums": "Mėgstami Albumai",
-    "HeaderFavoriteArtists": "Mėgstami Atlikėjai",
+    "HeaderFavoriteAlbums": "Mėgstami albumai",
+    "HeaderFavoriteArtists": "Mėgstami atlikėjai",
     "HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
     "HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
     "HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
     "HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
     "HeaderFavoriteSongs": "Mėgstamos Dainos",
     "HeaderFavoriteSongs": "Mėgstamos Dainos",
     "HeaderLiveTV": "Tiesioginė TV",
     "HeaderLiveTV": "Tiesioginė TV",
-    "HeaderNextUp": "Toliau eilėje",
+    "HeaderNextUp": "Toliau",
     "HeaderRecordingGroups": "Įrašų grupės",
     "HeaderRecordingGroups": "Įrašų grupės",
     "HomeVideos": "Namų vaizdo įrašai",
     "HomeVideos": "Namų vaizdo įrašai",
     "Inherit": "Paveldėti",
     "Inherit": "Paveldėti",
-    "ItemAddedWithName": "{0} - buvo įkeltas į mediateką",
-    "ItemRemovedWithName": "{0} - buvo pašalinta iš mediatekos",
+    "ItemAddedWithName": "{0} - buvo įkeltas į biblioteką",
+    "ItemRemovedWithName": "{0} - buvo pašalinta iš bibliotekos",
     "LabelIpAddressValue": "IP adresas: {0}",
     "LabelIpAddressValue": "IP adresas: {0}",
     "LabelRunningTimeValue": "Trukmė: {0}",
     "LabelRunningTimeValue": "Trukmė: {0}",
     "Latest": "Naujausi",
     "Latest": "Naujausi",
@@ -36,7 +36,7 @@
     "MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
     "MessageApplicationUpdatedTo": "\"Jellyfin Server\" buvo atnaujinta iki {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
     "MessageNamedServerConfigurationUpdatedWithValue": "Serverio nustatymai (skyrius {0}) buvo atnaujinti",
     "MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
     "MessageServerConfigurationUpdated": "Serverio nustatymai buvo atnaujinti",
-    "MixedContent": "Mixed content",
+    "MixedContent": "Mišrus turinys",
     "Movies": "Filmai",
     "Movies": "Filmai",
     "Music": "Muzika",
     "Music": "Muzika",
     "MusicVideos": "Muzikiniai vaizdo įrašai",
     "MusicVideos": "Muzikiniai vaizdo įrašai",
@@ -53,21 +53,21 @@
     "NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
     "NotificationOptionNewLibraryContent": "Naujas turinys įkeltas",
     "NotificationOptionPluginError": "Įskiepio klaida",
     "NotificationOptionPluginError": "Įskiepio klaida",
     "NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
     "NotificationOptionPluginInstalled": "Įskiepis įdiegtas",
-    "NotificationOptionPluginUninstalled": "Įskiepis pašalintas",
+    "NotificationOptionPluginUninstalled": "Įskiepis išdiegtas",
     "NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
     "NotificationOptionPluginUpdateInstalled": "Įskiepio atnaujinimas įdiegtas",
     "NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
     "NotificationOptionServerRestartRequired": "Reikalingas serverio perleidimas",
     "NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
     "NotificationOptionTaskFailed": "Suplanuotos užduoties klaida",
-    "NotificationOptionUserLockedOut": "Vartotojas užblokuotas",
+    "NotificationOptionUserLockedOut": "Naudotojas užblokuotas",
     "NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
     "NotificationOptionVideoPlayback": "Vaizdo įrašo atkūrimas pradėtas",
     "NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
     "NotificationOptionVideoPlaybackStopped": "Vaizdo įrašo atkūrimas sustabdytas",
     "Photos": "Nuotraukos",
     "Photos": "Nuotraukos",
-    "Playlists": "Grojaraštis",
-    "Plugin": "Plugin",
+    "Playlists": "Grojaraščiai",
+    "Plugin": "Įskiepis",
     "PluginInstalledWithName": "{0} buvo įdiegtas",
     "PluginInstalledWithName": "{0} buvo įdiegtas",
     "PluginUninstalledWithName": "{0} buvo pašalintas",
     "PluginUninstalledWithName": "{0} buvo pašalintas",
     "PluginUpdatedWithName": "{0} buvo atnaujintas",
     "PluginUpdatedWithName": "{0} buvo atnaujintas",
-    "ProviderValue": "Provider: {0}",
-    "ScheduledTaskFailedWithName": "{0} klaida",
+    "ProviderValue": "Paslaugos tiekėjas: {0}",
+    "ScheduledTaskFailedWithName": "{0} nepavyko",
     "ScheduledTaskStartedWithName": "{0} paleista",
     "ScheduledTaskStartedWithName": "{0} paleista",
     "ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
     "ServerNameNeedsToBeRestarted": "{0} reikia iš naujo paleisti",
     "Shows": "Laidos",
     "Shows": "Laidos",
@@ -76,65 +76,67 @@
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
     "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
     "SubtitleDownloadFailureFromForItem": "{1} subtitrai buvo nesėkmingai parsiųsti iš {0}",
     "Sync": "Sinchronizuoti",
     "Sync": "Sinchronizuoti",
-    "System": "System",
-    "TvShows": "TV Serialai",
-    "User": "User",
-    "UserCreatedWithName": "Vartotojas {0} buvo sukurtas",
-    "UserDeletedWithName": "Vartotojas {0} ištrintas",
+    "System": "Sistema",
+    "TvShows": "TV laidos",
+    "User": "Naudotojas",
+    "UserCreatedWithName": "Buvo sukurtas {0} naudotojas",
+    "UserDeletedWithName": "Naudotojas {0} ištrintas",
     "UserDownloadingItemWithValues": "{0} siunčiasi {1}",
     "UserDownloadingItemWithValues": "{0} siunčiasi {1}",
-    "UserLockedOutWithName": "Vartotojas {0} užblokuotas",
+    "UserLockedOutWithName": "Naudotojas {0} užblokuotas",
     "UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
     "UserOfflineFromDevice": "{0} buvo atjungtas nuo {1}",
     "UserOnlineFromDevice": "{0} prisijungęs iš {1}",
     "UserOnlineFromDevice": "{0} prisijungęs iš {1}",
-    "UserPasswordChangedWithName": "Slaptažodis pakeistas vartotojui {0}",
-    "UserPolicyUpdatedWithName": "Vartotojo {0} teisės buvo pakeistos",
+    "UserPasswordChangedWithName": "Slaptažodis pakeistas naudotojui {0}",
+    "UserPolicyUpdatedWithName": "Naudotojo {0} teisės buvo pakeistos",
     "UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
     "UserStartedPlayingItemWithValues": "{0} leidžia {1} į {2}",
     "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
     "UserStoppedPlayingItemWithValues": "{0} baigė leisti {1} į {2}",
     "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
     "ValueHasBeenAddedToLibrary": "{0} pridėtas į mediateką",
-    "ValueSpecialEpisodeName": "Ypatinga - {0}",
-    "VersionNumber": "Version {0}",
-    "TaskUpdatePluginsDescription": "Atsisiųsti ir įdiegti atnaujinimus priedams kuriem yra nustatytas automatiškas atnaujinimas.",
-    "TaskUpdatePlugins": "Atnaujinti Priedus",
+    "ValueSpecialEpisodeName": "Ypatingų - {0}",
+    "VersionNumber": "Versija {0}",
+    "TaskUpdatePluginsDescription": "Atsisiunčia ir įdiegia įskiepių, kurie sukonfigūruoti atnaujinti automatiškai, naujinius.",
+    "TaskUpdatePlugins": "Atnaujinti įskieius",
     "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
     "TaskDownloadMissingSubtitlesDescription": "Ieško trūkstamų subtitrų internete remiantis metaduomenų konfigūracija.",
     "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
     "TaskCleanTranscodeDescription": "Ištrina dienos senumo perkodavimo failus.",
-    "TaskCleanTranscode": "Išvalyti Perkodavimo Direktorija",
-    "TaskRefreshLibraryDescription": "Ieškoti naujų failų jūsų mediatekoje ir atnaujina metaduomenis.",
-    "TaskRefreshLibrary": "Skenuoti Mediateka",
+    "TaskCleanTranscode": "Išvalyti perkodavimo katalogą",
+    "TaskRefreshLibraryDescription": "Skenuoja medijos biblioteką, ieškodamas naujų failų, ir atnaujina metaduomenis.",
+    "TaskRefreshLibrary": "Skenuoti medijos biblioteką",
     "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
     "TaskDownloadMissingSubtitles": "Atsisiųsti trūkstamus subtitrus",
     "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
     "TaskRefreshChannelsDescription": "Atnaujina internetinių kanalų informaciją.",
     "TaskRefreshChannels": "Atnaujinti kanalus",
     "TaskRefreshChannels": "Atnaujinti kanalus",
-    "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų mediatekoje.",
-    "TaskRefreshPeople": "Atnaujinti Žmones",
+    "TaskRefreshPeopleDescription": "Atnaujina metaduomenis apie aktorius ir režisierius jūsų medijos bibliotekoje.",
+    "TaskRefreshPeople": "Atnaujinti žmones",
     "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
     "TaskCleanLogsDescription": "Ištrina žurnalo failus kurie yra senesni nei {0} dienos.",
-    "TaskCleanLogs": "Išvalyti Žurnalą",
-    "TaskRefreshChapterImagesDescription": "Sukuria miniatiūras vaizdo įrašam, kurie turi scenas.",
-    "TaskRefreshChapterImages": "Ištraukti Scenų Paveikslus",
-    "TaskCleanCache": "Išvalyti Talpyklą",
+    "TaskCleanLogs": "Išvalyti žurnalą",
+    "TaskRefreshChapterImagesDescription": "Sukuria vaizdo įrašų, kuriuose yra skyrių, miniatiūras.",
+    "TaskRefreshChapterImages": "Ištraukti skyrių vaizdus",
+    "TaskCleanCache": "Išvalyti talpyklą",
     "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
     "TaskCleanCacheDescription": "Ištrina talpyklos failus, kurių daugiau nereikia sistemai.",
-    "TasksChannelsCategory": "Internetiniai Kanalai",
+    "TasksChannelsCategory": "Internetiniai kanalai",
     "TasksApplicationCategory": "Programa",
     "TasksApplicationCategory": "Programa",
-    "TasksLibraryCategory": "Mediateka",
+    "TasksLibraryCategory": "Biblioteka",
     "TasksMaintenanceCategory": "Priežiūra",
     "TasksMaintenanceCategory": "Priežiūra",
     "TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
     "TaskCleanActivityLog": "Išvalyti veiklos žurnalą",
     "Undefined": "Neapibrėžtas",
     "Undefined": "Neapibrėžtas",
-    "Forced": "Priverstas",
+    "Forced": "Priverstinis",
     "Default": "Numatytas",
     "Default": "Numatytas",
-    "TaskCleanActivityLogDescription": "Ištrina veiklos žuranlo įrašus, kurie yra senesni nei nustatytas amžius.",
+    "TaskCleanActivityLogDescription": "Ištrina senesnius nei nustatytas amžius veiklos žurnalo įrašus.",
     "TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
     "TaskOptimizeDatabase": "Optimizuoti duomenų bazę",
     "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.",
     "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šgavėjas",
+    "TaskKeyframeExtractor": "Reikšminių kadrų (KeyFrame) išgavė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ą.",
     "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",
     "HearingImpaired": "Su klausos sutrikimais",
     "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
     "TaskRefreshTrickplayImages": "Generuoti Trickplay atvaizdus",
     "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
     "TaskRefreshTrickplayImagesDescription": "Sukuria trickplay peržiūras vaizdo įrašams įgalintose bibliotekose.",
-    "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis kolekcijose ir grojaraščiuose",
-    "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš kolekcijų ir grojaraščių.",
-    "TaskAudioNormalization": "Garso Normalizavimas",
-    "TaskAudioNormalizationDescription": "Skenuoti garso normalizavimo informacijos failuose.",
+    "TaskCleanCollectionsAndPlaylists": "Išvalo duomenis rinkiniuose ir grojaraščiuose",
+    "TaskCleanCollectionsAndPlaylistsDescription": "Pašalina neegzistuojančius elementus iš rinkinių ir grojaraščių.",
+    "TaskAudioNormalization": "Garso normalizavimas",
+    "TaskAudioNormalizationDescription": "Skenuoja failus, ieškant garso normalizavimo duomenų.",
     "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
     "TaskExtractMediaSegments": "Medijos segmentų nuskaitymas",
     "TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
     "TaskDownloadMissingLyrics": "Parsisiųsti trūkstamus dainų tekstus",
-    "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų papildinių.",
+    "TaskExtractMediaSegmentsDescription": "Ištraukia arba gauna medijos segmentus iš MediaSegment ijungtų įskiepių.",
     "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
     "TaskMoveTrickplayImages": "Pakeisti Trickplay vaizdų vietą",
     "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
     "TaskMoveTrickplayImagesDescription": "Perkelia egzistuojančius trickplay failus pagal bibliotekos nustatymus.",
-    "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius"
+    "TaskDownloadMissingLyricsDescription": "Parsisiųsti dainų žodžius",
+    "CleanupUserDataTask": "Naudotojo duomenų valymo užduotis",
+    "CleanupUserDataTaskDescription": "Iš medijos, kurios nebėra bent 90 dienų, išvalo visus naudotojo duomenis (žiūrėjimo būseną, mėgstamiausią būseną ir t. t.)."
 }
 }

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

@@ -135,5 +135,7 @@
     "TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
     "TaskMoveTrickplayImages": "Trickplay attēlu pārvietošana",
     "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
     "TaskMoveTrickplayImagesDescription": "Pārvieto esošos trickplay failus atbilstoši bibliotēkas iestatījumiem.",
     "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
     "TaskDownloadMissingLyrics": "Lejupielādēt trūkstošos vārdus",
-    "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām"
+    "TaskDownloadMissingLyricsDescription": "Lejupielādēt vārdus dziesmām",
+    "CleanupUserDataTask": "Lietotāju datu tīrīšanas uzdevums",
+    "CleanupUserDataTaskDescription": "Notīra visus lietotāja datus (skatīšanās stāvokļus, favorītu statusi utt.) no medijiem, kas vairs nav pieejami vismaz 90 dienas."
 }
 }

+ 6 - 1
Emby.Server.Implementations/Localization/Core/lzh.json

@@ -2,5 +2,10 @@
     "Albums": "辑册",
     "Albums": "辑册",
     "Artists": "艺人",
     "Artists": "艺人",
     "AuthenticationSucceededWithUserName": "{0} 授之权矣",
     "AuthenticationSucceededWithUserName": "{0} 授之权矣",
-    "Books": "册"
+    "Books": "册",
+    "Genres": "类",
+    "HeaderAlbumArtists": "辑者",
+    "Favorites": "至爱",
+    "Folders": "箧",
+    "HeaderContinueWatching": "接目未竟"
 }
 }

+ 131 - 4
Emby.Server.Implementations/Localization/Core/mn.json

@@ -1,14 +1,141 @@
 {
 {
     "Books": "Номууд",
     "Books": "Номууд",
-    "HeaderNextUp": "Дараах",
+    "HeaderNextUp": "Дараа нь",
     "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
     "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
     "Songs": "Дуунууд",
     "Songs": "Дуунууд",
     "Playlists": "Тоглуулах жагсаалт",
     "Playlists": "Тоглуулах жагсаалт",
     "Movies": "Кино",
     "Movies": "Кино",
     "Latest": "Сүүлийн үеийн",
     "Latest": "Сүүлийн үеийн",
-    "Genres": "Төрөл зүйл",
+    "Genres": "Төрлүүд",
     "Favorites": "Дуртай",
     "Favorites": "Дуртай",
     "Collections": "Багц",
     "Collections": "Багц",
-    "Artists": "Зураачуд",
-    "Albums": "Цомгууд"
+    "Artists": "Уран бүтээлчид",
+    "Albums": "Цомгууд",
+    "TaskExtractMediaSegments": "Медиа сегмент шалга",
+    "TaskExtractMediaSegmentsDescription": "MediaSegment идэвхжүүлсэн залгаасуудаас медиа сегментүүдийг задлах эсвэл олж авах.",
+    "TaskMoveTrickplayImages": "Трикплэй зургуудын байршлыг шилжүүлэх",
+    "TaskMoveTrickplayImagesDescription": "Одоогоор байгаа трикплэй файлуудыг сангийн тохиргоонд тохируулан шилжүүлнэ.",
+    "TaskDownloadMissingLyrics": "Алга болсон дууны үгийг татаж авах",
+    "TaskDownloadMissingLyricsDescription": "Дууны үгийг татаж авах",
+    "TaskOptimizeDatabase": "Датабаазыг сайжруулах",
+    "TaskKeyframeExtractor": "Түлхүүр кадр гаргагч",
+    "TaskCleanCache": "Кэш санг цэвэрлэх",
+    "NewVersionIsAvailable": "Jellyfin Server-н шинэ хувилбар татаж авахад нээлттэй боллоо.",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Server-н {0}-р хэсгийн тохиргоо шинэчлэгдлээ",
+    "NotificationOptionAudioPlaybackStopped": "Дууг зогсоов",
+    "NotificationOptionNewLibraryContent": "Шинэ агуулга орлоо",
+    "NotificationOptionServerRestartRequired": "Server-г дахин асаана уу",
+    "NotificationOptionVideoPlaybackStopped": "Бичлэгийг зогсоов",
+    "UserPasswordChangedWithName": "Хэрэглэгч {0}-н нууц үгийг өөрчиллөө",
+    "TaskCleanCollectionsAndPlaylists": "Цуглуулга ба тоглуулах жагсаалтыг цэвэрлэх",
+    "ScheduledTaskFailedWithName": "{0} амжилтгүй",
+    "StartupEmbyServerIsLoading": "Jellyfin Server ачааллаж байна. Хэсэг хугацааны дараа дахин оролдоно уу.",
+    "TaskCleanActivityLog": "Үйл ажиллагааны бүртгэлийг цэвэрлэх",
+    "SubtitleDownloadFailureFromForItem": "{0}-г {1}-д зориулсан хадмал орчуулгыг татаж авч чадсангүй",
+    "TaskRefreshLibraryDescription": "Таны медиа санг шинэ файлуудын хувьд шалгаж, мета мэдээллийг шинэчилнэ.",
+    "UserOfflineFromDevice": "{0}-г {1}-с салгалаа",
+    "ValueHasBeenAddedToLibrary": "{0}-г медиа сан руу нэмэгдлээ",
+    "TaskRefreshPeopleDescription": "Таны медиа санд байгаа жүжигчид болон найруулагчдын мета мэдээллийг шинэчилнэ.",
+    "TaskCleanTranscodeDescription": "Нэг өдрөөс илүү настай транскодлох файлуудыг устгана.",
+    "TaskRefreshChannelsDescription": "Интернет сувгуудын мэдээллийг шинэчлэх.",
+    "TaskDownloadMissingSubtitlesDescription": "Мета мэдээллийн тохиргоонд үндэслэн интернетээс алга болсон дэд гарчгийг хайна.",
+    "TaskOptimizeDatabaseDescription": "Мэдээллийн сантайг шахаж, чөлөөтэй зайг багасгана. Санг шалгаж, мэдээллийн сантай холбоотой өөрчлөлт хийхийн дараа энэ үйлдлийг гүйцэтгэх нь гүйцэтгэлийг сайжруулах боломжтой.",
+    "TaskKeyframeExtractorDescription": "Видео файлуудаас түлхүүр кадруудыг гаргаж, илүү нарийвчилсан HLS тоглуулах жагсаалт үүсгэнэ. Энэ үйлдэл удаан хугацаанд үргэлжлэх боломжтой.",
+    "NotificationOptionAudioPlayback": "Дууг тоглууллаа",
+    "TaskRefreshTrickplayImages": "Трикплэй зургуудыг үүсгэх",
+    "TaskUpdatePlugins": "Plugin-уудыг шинэчлэх",
+    "TaskCleanCollectionsAndPlaylistsDescription": "Одоо байхгүй болсон зүйлсийг цуглуулга ба тоглуулах жагсаалтаас устгана.",
+    "TaskAudioNormalization": "Аудиог хэвшүүлэх",
+    "TaskAudioNormalizationDescription": "Файлуудаас дууны хэвийн хэмжээсийн мэдээллийг шалгана.",
+    "TaskRefreshTrickplayImagesDescription": "Идэвхжсэн сангуудад байгаа видеонуудын трикплэй урьдчилсан харагдацыг үүсгэнэ.",
+    "TaskUpdatePluginsDescription": "Автомат шинэчлэлд тохируулсан залгаасуудын шинэчлэлтийг татаж авч суулгана.",
+    "TaskCleanTranscode": "Транскодлох санг цэвэрлэх",
+    "TaskRefreshChannels": "Сувгуудыг шинэчлэх",
+    "TaskDownloadMissingSubtitles": "Алга болсон хадмал орчуулгыг татах",
+    "External": "Гадны",
+    "HeaderFavoriteArtists": "Дуртай уран бүтээлчид",
+    "HeaderFavoriteEpisodes": "Дуртай ангиуд",
+    "HeaderFavoriteShows": "Дуртай нэвтрүүлэг",
+    "HeaderFavoriteSongs": "Дуртай дуу",
+    "AppDeviceValues": "Aпп: {0}, Төхөөрөмж: {1}",
+    "Application": "Aпп",
+    "AuthenticationSucceededWithUserName": "{0} амжилттай нэвтэрлээ",
+    "CameraImageUploadedFrom": "{0}-с шинэ зураг байршуулагдлаа",
+    "Channels": "Сувгууд",
+    "ChapterNameValue": "{0}-р бүлэг",
+    "Default": "Өгөгдмөл",
+    "DeviceOfflineWithName": "{0}-н холболт саллаа",
+    "DeviceOnlineWithName": "{0} холбогдлоо",
+    "FailedLoginAttemptWithUserName": "{0}-н нэвтрэх оролдлого амжилтгүй",
+    "Folders": "Хавтаснууд",
+    "Forced": "Хүчээр",
+    "HeaderAlbumArtists": "Цомгийн уран бүтээлчид",
+    "HeaderFavoriteAlbums": "Дуртай цомгууд",
+    "HeaderLiveTV": "Шууд",
+    "HeaderRecordingGroups": "Бичлэгийн бүлгүүд",
+    "HearingImpaired": "Сонсголын бэрхшээлтэй",
+    "HomeVideos": "Үндсэн дүрсүүд",
+    "Inherit": "Уламжлах",
+    "ItemAddedWithName": "{0}-г санд нэмлээ",
+    "ItemRemovedWithName": "{0}-с сангаас хаслаа",
+    "LabelIpAddressValue": "IP хаяг: {0}",
+    "LabelRunningTimeValue": "Үргэлжлэх хугацаа: {0}",
+    "MessageApplicationUpdated": "Jellyfin Server шинэчлэгдлээ",
+    "MessageApplicationUpdatedTo": "Jellyfin Server {0} болж шинэчлэгдлээ",
+    "MessageServerConfigurationUpdated": "Server-н тохиргоо шинэчлэгдлээ",
+    "MixedContent": "Холимог агуулга",
+    "Music": "Дуу",
+    "MusicVideos": "Дууны клип",
+    "NameInstallFailed": "{0} суулгахад алдаа гарлаа",
+    "NameSeasonNumber": "{0}-р улирал",
+    "NameSeasonUnknown": "Улирал олдсонгүй",
+    "NotificationOptionApplicationUpdateAvailable": "Апп шинэчлэлт бий болсон байна",
+    "NotificationOptionApplicationUpdateInstalled": "Апп-н шинэчлэлийг суулгалаа",
+    "NotificationOptionCameraImageUploaded": "Камерын зураг орууллаа",
+    "NotificationOptionInstallationFailed": "Суулгалт амжилтгүй",
+    "NotificationOptionPluginError": "Plugin-д алдаа гарлаа",
+    "NotificationOptionPluginInstalled": "Plugin-г суулгалаа",
+    "NotificationOptionPluginUninstalled": "Plugin-г устгалаа",
+    "NotificationOptionPluginUpdateInstalled": "Plugin-ны шинэчлэн суулгалаа",
+    "NotificationOptionTaskFailed": "Товолсон ажил амжилтгүй",
+    "NotificationOptionUserLockedOut": "Хэрэглэгчийг түгжив",
+    "NotificationOptionVideoPlayback": "Бичлэгийг тоглуулж эхлэв",
+    "Photos": "Зургууд",
+    "Plugin": "Plugin",
+    "PluginInstalledWithName": "{0}-г суулгалаа",
+    "PluginUninstalledWithName": "{0}-г устгалаа",
+    "PluginUpdatedWithName": "{0}-г шинэчиллээ",
+    "ProviderValue": "Нийлүүлэгч: {0}",
+    "ScheduledTaskStartedWithName": "{0}-г эхлүүлэв",
+    "ServerNameNeedsToBeRestarted": "{0}-г дахин асаана уу",
+    "Shows": "Нэвтрүүлгүүд",
+    "Sync": "Дахин",
+    "System": "Систем",
+    "TvShows": "ТВ нэвтрүүлгүүд",
+    "Undefined": "Танисангүй",
+    "User": "Хэрэглэгч",
+    "UserCreatedWithName": "Хэрэглэгч {0}-г үүсгэлээ",
+    "UserDeletedWithName": "Хэрэглэгч {0}-г устгалаа",
+    "UserDownloadingItemWithValues": "{0} нь {1}-г татаж байна",
+    "UserLockedOutWithName": "Хэрэглэгч {0}-г түгжлээ",
+    "UserOnlineFromDevice": "{0} нь {1}-тэй холбоотой байна",
+    "UserPolicyUpdatedWithName": "Хэрэглэгчийн журмыг {0}-д зориулан шинэчиллээ",
+    "UserStartedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж байна",
+    "UserStoppedPlayingItemWithValues": "{0}-г {2} дээр {1}-г тоглуулж дуусгалаа",
+    "ValueSpecialEpisodeName": "Тусгай - {0}",
+    "VersionNumber": "Хувилбар {0}",
+    "TasksMaintenanceCategory": "Засвар",
+    "TasksLibraryCategory": "Сан",
+    "TasksApplicationCategory": "Апп",
+    "TasksChannelsCategory": "Интернет сувгууд",
+    "TaskCleanActivityLogDescription": "Тохируулсан хугацаанаас хуучин үйл ажиллагааны бүртгэлийн бичлэгүүдийг устгана.",
+    "TaskCleanLogs": "Бүртгэлийн санг цэвэрлэх",
+    "TaskCleanLogsDescription": "{0} өдрөөс илүү настай бүртгэлийн файлуудыг устгана.",
+    "TaskRefreshPeople": "Хүмүүсийг шинэчлэх",
+    "TaskCleanCacheDescription": "Системд хэрэггүй болсон кэш файлуудыг устгана.",
+    "TaskRefreshChapterImages": "Бүлгийн зураг авах",
+    "TaskRefreshChapterImagesDescription": "Бүлгүүдтэй видеонуудын хуудсан зураг үүсгэнэ.",
+    "TaskRefreshLibrary": "Медиа санг шалгах",
+    "CleanupUserDataTask": "Хэрэглэгчийн өгөгдлийн цэвэрлэгээний үүрэг",
+    "CleanupUserDataTaskDescription": "Хугацаа нь 90 хоногоос дээш хугацаанд байхгүй болсон медианаас бүх хэрэглэгчийн өгөгдлийг (үзсэн төлөв, дуртай жагсаалт гэх мэт) цэвэрлэнэ."
 }
 }

+ 9 - 2
Emby.Server.Implementations/Localization/Core/mr.json

@@ -118,12 +118,19 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे",
     "MessageNamedServerConfigurationUpdatedWithValue": "सर्व्हर कॉन्फिगरेशन विभाग {0} अद्यतनित केला गेला आहे",
     "Inherit": "वारसा",
     "Inherit": "वारसा",
     "Forced": "सक्ती केली आहे",
     "Forced": "सक्ती केली आहे",
-    "FailedLoginAttemptWithUserName": "अयशस्वी लॉगिन {0} पासून प्रयत्न करा",
+    "FailedLoginAttemptWithUserName": "{0} कडून लॉगिन करण्याचा प्रयत्न अयशस्वी झाला",
     "External": "बाहेरचा",
     "External": "बाहेरचा",
     "DeviceOnlineWithName": "{0} कनेक्ट झाले",
     "DeviceOnlineWithName": "{0} कनेक्ट झाले",
     "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
     "DeviceOfflineWithName": "{0} डिस्कनेक्ट झाला आहे",
     "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
     "AuthenticationSucceededWithUserName": "{0} यशस्वीरित्या प्रमाणीकृत",
     "HearingImpaired": "कर्णबधीर",
     "HearingImpaired": "कर्णबधीर",
     "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
     "TaskRefreshTrickplayImages": "ट्रिकप्ले प्रतिमा तयार करा",
-    "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते."
+    "TaskRefreshTrickplayImagesDescription": "सक्षम लायब्ररीमधील व्हिडिओंसाठी ट्रिकप्ले पूर्वावलोकन तयार करते.",
+    "TaskCleanCollectionsAndPlaylists": "संग्रह आणि प्लेलिस्ट व्यवस्थित करा",
+    "TaskExtractMediaSegments": "मिडिया विभाग तपासणी",
+    "TaskMoveTrickplayImages": "ट्रिकप्ले प्रतिमेचे स्थान स्थलांतर करा",
+    "TaskDownloadMissingLyrics": "उपलब्ध नसलेली गीतपट्टी (Lyrics) डाउनलोड करा",
+    "TaskAudioNormalization": "ऑडिओ सामान्यीकरण",
+    "TaskAudioNormalizationDescription": "ऑडिओ सामान्यीकरणाचा डाटा स्कॅन करतो.",
+    "TaskDownloadMissingLyricsDescription": "गाण्यांची गीतपट्टी (Lyrics) डाउनलोड करतो"
 }
 }

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