2
0
Эх сурвалжийг харах

Merge branch 'jellyfin:master' into master

Ronan Charles-Lorel 1 жил өмнө
parent
commit
e108183b13
100 өөрчлөгдсөн 1610 нэмэгдсэн , 1200 устгасан
  1. 1 1
      .ci/azure-pipelines-package.yml
  2. 10 6
      .github/ISSUE_TEMPLATE/issue report.yml
  3. 1 0
      .github/workflows/automation.yml
  4. 5 5
      .github/workflows/codeql-analysis.yml
  5. 7 7
      .github/workflows/commands.yml
  6. 14 12
      .github/workflows/openapi.yml
  7. 24 3
      .github/workflows/repo-stale.yaml
  8. 0 3
      .npmrc
  9. 5 0
      CONTRIBUTORS.md
  10. 92 0
      Directory.Packages.props
  11. 2 1
      Dockerfile
  12. 1 0
      Dockerfile.arm
  13. 1 0
      Dockerfile.arm64
  14. 1 3
      Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs
  15. 1 3
      Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs
  16. 12 8
      Emby.Dlna/Didl/DidlBuilder.cs
  17. 5 5
      Emby.Dlna/Emby.Dlna.csproj
  18. 1 1
      Emby.Dlna/Eventing/DlnaEventManager.cs
  19. 0 1
      Emby.Dlna/Main/DlnaEntryPoint.cs
  20. 40 9
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  21. 35 55
      Emby.Dlna/PlayTo/PlayToController.cs
  22. 2 3
      Emby.Dlna/PlayTo/PlayToManager.cs
  23. 6 6
      Emby.Dlna/PlayTo/TransportCommands.cs
  24. 18 8
      Emby.Dlna/Server/DescriptionXmlBuilder.cs
  25. 2 7
      Emby.Naming/Audio/AlbumParser.cs
  26. 3 3
      Emby.Naming/AudioBook/AudioBookFilePathParser.cs
  27. 10 10
      Emby.Naming/AudioBook/AudioBookListResolver.cs
  28. 2 2
      Emby.Naming/AudioBook/AudioBookNameParser.cs
  29. 12 37
      Emby.Naming/Common/NamingOptions.cs
  30. 5 5
      Emby.Naming/Emby.Naming.csproj
  31. 7 7
      Emby.Naming/TV/EpisodePathParser.cs
  32. 1 1
      Emby.Naming/TV/SeriesResolver.cs
  33. 1 1
      Emby.Naming/Video/CleanDateTimeParser.cs
  34. 1 1
      Emby.Naming/Video/ExtraRuleResolver.cs
  35. 1 1
      Emby.Naming/Video/FileStackRule.cs
  36. 36 20
      Emby.Naming/Video/VideoListResolver.cs
  37. 1 2
      Emby.Naming/Video/VideoResolver.cs
  38. 5 5
      Emby.Photos/Emby.Photos.csproj
  39. 17 35
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  40. 20 77
      Emby.Server.Implementations/ApplicationHost.cs
  41. 20 19
      Emby.Server.Implementations/Channels/ChannelManager.cs
  42. 13 8
      Emby.Server.Implementations/Collections/CollectionManager.cs
  43. 1 3
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  44. 4 3
      Emby.Server.Implementations/ConfigurationOptions.cs
  45. 77 48
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  46. 79 0
      Emby.Server.Implementations/Data/ConnectionPool.cs
  47. 6 7
      Emby.Server.Implementations/Data/ManagedConnection.cs
  48. 226 256
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  49. 11 27
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  50. 18 19
      Emby.Server.Implementations/Dto/DtoService.cs
  51. 15 15
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  52. 24 17
      Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs
  53. 7 6
      Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
  54. 14 2
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  55. 0 13
      Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs
  56. 11 35
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  57. 0 2
      Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
  58. 0 2
      Emby.Server.Implementations/Images/FolderImageProvider.cs
  59. 0 2
      Emby.Server.Implementations/Images/GenreImageProvider.cs
  60. 36 26
      Emby.Server.Implementations/Library/LibraryManager.cs
  61. 2 2
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  62. 80 18
      Emby.Server.Implementations/Library/PathExtensions.cs
  63. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  64. 5 2
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  65. 9 6
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  66. 21 5
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  67. 13 4
      Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
  68. 4 2
      Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
  69. 9 13
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  70. 14 4
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  71. 7 4
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  72. 4 2
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  73. 18 8
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  74. 6 0
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  75. 18 8
      Emby.Server.Implementations/Library/UserViewManager.cs
  76. 5 12
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  77. 11 14
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  78. 12 11
      Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs
  79. 16 16
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  80. 3 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  81. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  82. 1 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
  83. 14 7
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  84. 20 27
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  85. 11 36
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  86. 125 2
      Emby.Server.Implementations/Localization/Core/be.json
  87. 22 16
      Emby.Server.Implementations/Localization/Core/bn.json
  88. 42 42
      Emby.Server.Implementations/Localization/Core/ca.json
  89. 3 2
      Emby.Server.Implementations/Localization/Core/cy.json
  90. 52 52
      Emby.Server.Implementations/Localization/Core/da.json
  91. 2 2
      Emby.Server.Implementations/Localization/Core/es-AR.json
  92. 1 1
      Emby.Server.Implementations/Localization/Core/es.json
  93. 2 1
      Emby.Server.Implementations/Localization/Core/es_419.json
  94. 2 1
      Emby.Server.Implementations/Localization/Core/fa.json
  95. 1 1
      Emby.Server.Implementations/Localization/Core/fi.json
  96. 5 1
      Emby.Server.Implementations/Localization/Core/fil.json
  97. 8 4
      Emby.Server.Implementations/Localization/Core/gsw.json
  98. 57 1
      Emby.Server.Implementations/Localization/Core/hi.json
  99. 1 1
      Emby.Server.Implementations/Localization/Core/id.json
  100. 10 1
      Emby.Server.Implementations/Localization/Core/is.json

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

@@ -47,7 +47,7 @@ jobs:
     displayName: Set release version (stable)
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
 
-  - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) deployment'
+  - script: 'docker build -f deployment/Dockerfile.$(BuildConfiguration) -t jellyfin-server-$(BuildConfiguration) --label "org.opencontainers.image.url=$(Build.Repository.Uri)" --label "org.opencontainers.image.revision=$(Build.SourceVersion)"  deployment'
     displayName: 'Build Dockerfile'
 
   - script: 'docker image ls -a && docker run -v $(pwd)/deployment/dist:/dist -v $(pwd):/jellyfin -e IS_UNSTABLE="yes" -e BUILD_ID=$(Build.BuildNumber) jellyfin-server-$(BuildConfiguration)'

+ 10 - 6
.github/ISSUE_TEMPLATE/issue report.yml

@@ -30,9 +30,9 @@ body:
       label: Jellyfin Version
       description: What version of Jellyfin are you running?
       options:
-        - 10.8.0
+        - 10.8.z
+        - 10.8.9
         - 10.7.7
-        - 10.7.z
         - 10.6.4
         - Other
     validations:
@@ -47,13 +47,15 @@ body:
       label: Environment
       description: |
         Examples:
-        - **OS**: [e.g. Debian, Windows]
+        - **OS**: [e.g. Debian 11, Windows 10]
+        - **Linux Kernel**: [e.g. none, 5.15, 6.1, etc.]
         - **Virtualization**: [e.g. Docker, KVM, LXC]
         - **Clients**: [Browser, Android, Fire Stick, etc.]
         - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
-        - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+        - **FFmpeg Version**: [e.g. 5.1.2-Jellyfin]
         - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
         - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+        - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
         - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
         - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
         - **Base URL**: [e.g. none, yes: /example]
@@ -61,12 +63,14 @@ body:
         - **Storage**: [e.g. local, NFS, cloud]
       value: |
         - OS:
+        - Linux Kernel:
         - Virtualization:
         - Clients:
         - Browser:
         - FFmpeg Version:
         - Playback Method:
         - Hardware Acceleration:
+        - GPU Model:
         - Plugins:
         - Reverse Proxy:
         - Base URL:
@@ -84,8 +88,8 @@ body:
     id: ffmpeg-logs
     attributes:
       label: FFmpeg logs
-      description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
-      placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+      description: Please copy and paste recent FFmpeg log output. This can be found in Dashboard > Logs > FFmpeg*.log.
+      placeholder: This field is mandatory for debugging hardware transcoding issues. It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
       render: shell
   - type: textarea
     id: browserlogs

+ 1 - 0
.github/workflows/automation.yml

@@ -19,6 +19,7 @@ jobs:
         if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
         with:
           dirtyLabel: 'merge conflict'
+          commentOnDirty: 'This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.'
           repoToken: ${{ secrets.JF_BOT_TOKEN }}
 
   project:

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

@@ -20,18 +20,18 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+      uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
     - name: Setup .NET
-      uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+      uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
       with:
         dotnet-version: '7.0.x'
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+      uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+      uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+      uses: github/codeql-action/analyze@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1

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

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

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

@@ -14,18 +14,18 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
       - name: Setup .NET
-        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+        uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
         with:
           dotnet-version: '7.0.x'
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         with:
           name: openapi-head
           retention-days: 14
@@ -39,25 +39,27 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
           fetch-depth: 0
       - name: Checkout common ancestor
+        env:
+          HEAD_REF: ${{ github.head_ref }}
         run: |
           git remote add upstream https://github.com/${{ github.event.pull_request.base.repo.full_name }}
           git -c protocol.version=2 fetch --prune --progress --no-recurse-submodules upstream +refs/heads/*:refs/remotes/upstream/* +refs/tags/*:refs/tags/*
-          ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
+          ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF)
           git checkout --progress --force $ANCESTOR_REF
       - name: Setup .NET
-        uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+        uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
         with:
           dotnet-version: '7.0.x'
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
+        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
         with:
           name: openapi-base
           retention-days: 14
@@ -76,12 +78,12 @@ jobs:
       - openapi-base
     steps:
       - name: Download openapi-head
-        uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+        uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
         with:
           name: openapi-head
           path: openapi-head
       - name: Download openapi-base
-        uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3
+        uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
         with:
           name: openapi-base
           path: openapi-base
@@ -103,14 +105,14 @@ jobs:
           body="${body//$'\r'/'%0D'}"
           echo ::set-output name=body::$body
       - name: Find difference comment
-        uses: peter-evans/find-comment@81e2da3af01c92f83cb927cf3ace0e085617c556 # v2
+        uses: peter-evans/find-comment@a54c31d7fa095754bfef525c0c8e5e5674c4b4b1 # v2.4.0
         id: find-comment
         with:
           issue-number: ${{ github.event.pull_request.number }}
           direction: last
           body-includes: openapi-diff-workflow-comment
       - name: Reply or edit difference comment (changed)
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ steps.read-diff.outputs.body != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}
@@ -125,7 +127,7 @@ jobs:
 
             </details>
       - name: Edit difference comment (unchanged)
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}

+ 24 - 3
.github/workflows/repo-stale.yaml

@@ -1,4 +1,4 @@
-name: Issue Stale Check
+name: Stale Check
 
 on:
   schedule:
@@ -7,12 +7,15 @@ on:
 
 permissions:
   issues: write
+  pull-requests: write
+
 jobs:
-  stale:
+  issues:
+    name: Check issues
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
-      - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
+      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           days-before-stale: 120
@@ -28,3 +31,21 @@ jobs:
             If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
 
             This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
+
+  prs-conflicts:
+    name: Check PRs with merge conflicts
+    runs-on: ubuntu-latest
+    if: ${{ contains(github.repository, 'jellyfin/') }}
+    steps:
+      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
+        with:
+          repo-token: ${{ secrets.JF_BOT_TOKEN }}
+          operations-per-run: 75
+          # The merge conflict action will remove the label when updated
+          remove-stale-when-updated: false
+          days-before-stale: -1
+          days-before-close: 90
+          days-before-issue-close: -1
+          stale-pr-label: merge conflict
+          close-pr-message: |-
+            This PR has been closed due to having unresolved merge conflicts.

+ 0 - 3
.npmrc

@@ -1,3 +0,0 @@
-registry=https://registry.npmjs.org/
-@jellyfin:registry=https://pkgs.dev.azure.com/jellyfin-project/jellyfin/_packaging/unstable/npm/registry/
-always-auth=true

+ 5 - 0
CONTRIBUTORS.md

@@ -58,6 +58,7 @@
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [ikomhoog](https://github.com/ikomhoog)
  - [jftuga](https://github.com/jftuga)
+ - [jmshrv](https://github.com/jmshrv)
  - [joern-h](https://github.com/joern-h)
  - [joshuaboniface](https://github.com/joshuaboniface)
  - [JustAMan](https://github.com/JustAMan)
@@ -125,6 +126,7 @@
  - [SuperSandro2000](https://github.com/SuperSandro2000)
  - [tbraeutigam](https://github.com/tbraeutigam)
  - [teacupx](https://github.com/teacupx)
+ - [TelepathicWalrus](https://github.com/TelepathicWalrus)
  - [Terror-Gene](https://github.com/Terror-Gene)
  - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
  - [ThibaultNocchi](https://github.com/ThibaultNocchi)
@@ -162,6 +164,8 @@
  - [vgambier](https://github.com/vgambier)
  - [MinecraftPlaye](https://github.com/MinecraftPlaye)
  - [RealGreenDragon](https://github.com/RealGreenDragon)
+ - [ipitio](https://github.com/ipitio)
+ - [TheTyrius](https://github.com/TheTyrius)
 
 # Emby Contributors
 
@@ -231,3 +235,4 @@
  - [Matthew Jones](https://github.com/matthew-jones-uk)
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Utku Özdemir](https://github.com/utkuozdemir)
+ - [JPUC1143](https://github.com/Jpuc1143/)

+ 92 - 0
Directory.Packages.props

@@ -0,0 +1,92 @@
+<Project>
+  <PropertyGroup>
+    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+  </PropertyGroup>
+
+  <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
+
+  <ItemGroup Label="Package Dependencies">
+    <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
+    <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
+    <PackageVersion Include="AutoFixture" Version="4.18.0" />
+    <PackageVersion Include="BDInfo" Version="0.7.6.2" />
+    <PackageVersion Include="BlurHashSharp.SkiaSharp" Version="1.2.0" />
+    <PackageVersion Include="BlurHashSharp" Version="1.2.0" />
+    <PackageVersion Include="CommandLineParser" Version="2.9.1" />
+    <PackageVersion Include="coverlet.collector" Version="6.0.0" />
+    <PackageVersion Include="Diacritics" Version="3.3.18" />
+    <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
+    <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
+    <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
+    <PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
+    <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
+    <PackageVersion Include="libse" Version="3.6.13" />
+    <PackageVersion Include="LrcParser" Version="2023.524.0" />
+    <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="7.0.4" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.8" />
+    <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
+    <PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
+    <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
+    <PackageVersion Include="MimeTypes" Version="2.4.0" />
+    <PackageVersion Include="Mono.Nat" Version="3.0.4" />
+    <PackageVersion Include="Moq" Version="4.18.4" />
+    <PackageVersion Include="NEbml" Version="0.11.0" />
+    <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
+    <PackageVersion Include="PlaylistsNET" Version="1.4.0" />
+    <PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
+    <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
+    <PackageVersion Include="prometheus-net" Version="8.0.0" />
+    <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
+    <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
+    <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.0" />
+    <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
+    <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
+    <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
+    <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.1" />
+    <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
+    <PackageVersion Include="SharpFuzz" Version="2.1.0" />
+    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
+    <PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
+    <PackageVersion Include="SkiaSharp" Version="2.88.3" />
+    <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
+    <PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
+    <PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
+    <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
+    <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.0" />
+    <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
+    <PackageVersion Include="System.Globalization" Version="4.3.0" />
+    <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
+    <PackageVersion Include="System.Text.Encoding.CodePages" Version="7.0.0" />
+    <PackageVersion Include="System.Text.Json" Version="7.0.3" />
+    <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="7.0.0" />
+    <PackageVersion Include="TagLibSharp" Version="2.3.0" />
+    <PackageVersion Include="TMDbLib" Version="2.0.0" />
+    <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
+    <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
+    <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
+    <PackageVersion Include="xunit" Version="2.4.2" />
+  </ItemGroup>
+</Project>

+ 2 - 1
Dockerfile

@@ -10,6 +10,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
 
 FROM debian:stable-slim as app
@@ -37,7 +38,7 @@ RUN apt-get update \
  && apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
    mesa-va-drivers \
-   jellyfin-ffmpeg \
+   jellyfin-ffmpeg5 \
    openssl \
    locales \
 # Intel VAAPI Tone mapping dependencies:

+ 1 - 0
Dockerfile.arm

@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu

+ 1 - 0
Dockerfile.arm64

@@ -11,6 +11,7 @@ RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu

+ 1 - 3
Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs

@@ -27,7 +27,7 @@ namespace Emby.Dlna.ConnectionManager
         /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         {
-            var list = new List<StateVariable>
+            return new StateVariable[]
             {
                 new StateVariable
                 {
@@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
                     SendsEvents = false
                 }
             };
-
-            return list;
         }
     }
 }

+ 1 - 3
Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs

@@ -27,7 +27,7 @@ namespace Emby.Dlna.ContentDirectory
         /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         {
-            var list = new List<StateVariable>
+            return new StateVariable[]
             {
                 new StateVariable
                 {
@@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
                     SendsEvents = false
                 }
             };
-
-            return list;
         }
     }
 }

+ 12 - 8
Emby.Dlna/Didl/DidlBuilder.cs

@@ -10,6 +10,7 @@ using System.Text;
 using System.Xml;
 using Emby.Dlna.ContentDirectory;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
@@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
 
             var types = new[]
             {
-                PersonType.Director,
-                PersonType.Writer,
-                PersonType.Producer,
-                PersonType.Composer,
-                "creator"
+                PersonKind.Director,
+                PersonKind.Writer,
+                PersonKind.Producer,
+                PersonKind.Composer,
+                PersonKind.Creator
             };
 
             // Seeing some LG models locking up due content with large lists of people
@@ -888,10 +889,13 @@ namespace Emby.Dlna.Didl
 
             foreach (var actor in people)
             {
-                var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase))
-                    ?? PersonType.Actor;
+                var type = types.FirstOrDefault(i => i == actor.Type || string.Equals(actor.Role, i.ToString(), StringComparison.OrdinalIgnoreCase));
+                if (type == PersonKind.Unknown)
+                {
+                    type = PersonKind.Actor;
+                }
 
-                AddValue(writer, "upnp", type.ToLowerInvariant(), actor.Name, NsUpnp);
+                AddValue(writer, "upnp", type.ToString().ToLowerInvariant(), actor.Name, NsUpnp);
             }
         }
 

+ 5 - 5
Emby.Dlna/Emby.Dlna.csproj

@@ -28,13 +28,13 @@
 
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
 
   <ItemGroup>
@@ -80,7 +80,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" />
   </ItemGroup>
 
 </Project>

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

@@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
 
             try
             {
-                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
                     .SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
             }
             catch (OperationCanceledException)

+ 0 - 1
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -7,7 +7,6 @@ using System.Globalization;
 using System.Linq;
 using System.Net.Http;
 using System.Net.Sockets;
-using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.Ssdp;

+ 40 - 9
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -2,9 +2,11 @@
 
 using System;
 using System.Globalization;
+using System.IO;
 using System.Net.Http;
 using System.Net.Mime;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Xml;
@@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Dlna.PlayTo
 {
-    public class DlnaHttpClient
+    /// <summary>
+    /// Http client for Dlna PlayTo function.
+    /// </summary>
+    public partial class DlnaHttpClient
     {
         private readonly ILogger _logger;
         private readonly IHttpClientFactory _httpClientFactory;
@@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
 
         private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
         {
-            using var response = await _httpClientFactory.CreateClient(NamedClient.Dlna).SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+            var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
+            using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
             response.EnsureSuccessStatusCode();
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            await using MemoryStream ms = new MemoryStream();
+            await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
             try
             {
                 return await XDocument.LoadAsync(
-                    stream,
+                    ms,
                     LoadOptions.None,
                     cancellationToken).ConfigureAwait(false);
             }
-            catch (XmlException ex)
+            catch (XmlException)
             {
-                _logger.LogError(ex, "Failed to parse response");
-                if (_logger.IsEnabled(LogLevel.Debug))
+                // try correcting the Xml response with common errors
+                ms.Position = 0;
+                using StreamReader sr = new StreamReader(ms);
+                var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+
+                // find and replace unescaped ampersands (&)
+                xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
+
+                try
                 {
-                    _logger.LogDebug("Malformed response: {Content}\n", await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false));
+                    // retry reading Xml
+                    using var xmlReader = new StringReader(xmlString);
+                    return await XDocument.LoadAsync(
+                        xmlReader,
+                        LoadOptions.None,
+                        cancellationToken).ConfigureAwait(false);
                 }
+                catch (XmlException ex)
+                {
+                    _logger.LogError(ex, "Failed to parse response");
+                    _logger.LogDebug("Malformed response: {Content}\n", xmlString);
 
-                return null;
+                    return null;
+                }
             }
         }
 
@@ -104,5 +128,12 @@ namespace Emby.Dlna.PlayTo
             // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
             return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
         }
+
+        /// <summary>
+        /// Compile-time generated regular expression for escaping ampersands.
+        /// </summary>
+        /// <returns>Compiled regular expression.</returns>
+        [GeneratedRegex("(&(?![a-z]*;))")]
+        private static partial Regex EscapeAmpersandRegex();
     }
 }

+ 35 - 55
Emby.Dlna/PlayTo/PlayToController.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
             IUserDataManager userDataManager,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
-            IMediaEncoder mediaEncoder)
+            IMediaEncoder mediaEncoder,
+            Device device)
         {
             _session = session;
             _sessionManager = sessionManager;
@@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
             _localization = localization;
             _mediaSourceManager = mediaSourceManager;
             _mediaEncoder = mediaEncoder;
-        }
-
-        public bool IsSessionActive => !_disposed && _device is not null;
 
-        public bool SupportsMediaControl => IsSessionActive;
-
-        public void Init(Device device)
-        {
             _device = device;
             _device.OnDeviceUnavailable = OnDeviceUnavailable;
             _device.PlaybackStart += OnDevicePlaybackStart;
@@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
             _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
         }
 
+        public bool IsSessionActive => !_disposed;
+
+        public bool SupportsMediaControl => IsSessionActive;
+
         /*
          * Send a message to the DLNA device to notify what is the next track in the playlist.
          */
@@ -131,22 +127,22 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private void OnDeviceDiscoveryDeviceLeft(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+        private void OnDeviceDiscoveryDeviceLeft(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
         {
             var info = e.Argument;
 
             if (!_disposed
-                && info.Headers.TryGetValue("USN", out string usn)
+                && info.Headers.TryGetValue("USN", out string? usn)
                 && usn.IndexOf(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
                 && (usn.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1
-                    || (info.Headers.TryGetValue("NT", out string nt)
+                    || (info.Headers.TryGetValue("NT", out string? nt)
                         && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
             {
                 OnDeviceUnavailable();
             }
         }
 
-        private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
+        private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
         {
             if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
             {
@@ -188,7 +184,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private async void OnDevicePlaybackStopped(object sender, PlaybackStoppedEventArgs e)
+        private async void OnDevicePlaybackStopped(object? sender, PlaybackStoppedEventArgs e)
         {
             if (_disposed)
             {
@@ -257,7 +253,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private async void OnDevicePlaybackStart(object sender, PlaybackStartEventArgs e)
+        private async void OnDevicePlaybackStart(object? sender, PlaybackStartEventArgs e)
         {
             if (_disposed)
             {
@@ -281,7 +277,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private async void OnDevicePlaybackProgress(object sender, PlaybackProgressEventArgs e)
+        private async void OnDevicePlaybackProgress(object? sender, PlaybackProgressEventArgs e)
         {
             if (_disposed)
             {
@@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
 
         private PlaylistItem CreatePlaylistItem(
             BaseItem item,
-            User user,
+            User? user,
             long startPostionTicks,
-            string mediaSourceId,
+            string? mediaSourceId,
             int? audioStreamIndex,
             int? subtitleStreamIndex)
         {
@@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
             return playlistItem;
         }
 
-        private string GetDlnaHeaders(PlaylistItem item)
+        private string? GetDlnaHeaders(PlaylistItem item)
         {
             var profile = item.Profile;
             var streamInfo = item.StreamInfo;
@@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
+        private PlaylistItem GetPlaylistItem(BaseItem item, MediaSourceInfo[] mediaSources, DeviceProfile profile, string deviceId, string? mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
         {
             if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
             {
@@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
             _device.MediaChanged -= OnDeviceMediaChanged;
             _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
             _device.OnDeviceUnavailable = null;
-            _device = null;
 
             _disposed = true;
         }
@@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
                 case GeneralCommandType.ToggleMute:
                     return _device.ToggleMute(cancellationToken);
                 case GeneralCommandType.SetAudioStreamIndex:
-                    if (command.Arguments.TryGetValue("Index", out string index))
+                    if (command.Arguments.TryGetValue("Index", out string? index))
                     {
                         if (int.TryParse(index, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
                         {
@@ -740,7 +735,7 @@ namespace Emby.Dlna.PlayTo
 
                     throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
                 case GeneralCommandType.SetVolume:
-                    if (command.Arguments.TryGetValue("Volume", out string vol))
+                    if (command.Arguments.TryGetValue("Volume", out string? vol))
                     {
                         if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
                         {
@@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
                 throw new ObjectDisposedException(GetType().Name);
             }
 
-            if (_device is null)
-            {
-                return Task.CompletedTask;
-            }
-
-            if (name == SessionMessageType.Play)
-            {
-                return SendPlayCommand(data as PlayRequest, cancellationToken);
-            }
-
-            if (name == SessionMessageType.Playstate)
+            return name switch
             {
-                return SendPlaystateCommand(data as PlaystateRequest, cancellationToken);
-            }
-
-            if (name == SessionMessageType.GeneralCommand)
-            {
-                return SendGeneralCommand(data as GeneralCommand, cancellationToken);
-            }
-
-            // Not supported or needed right now
-            return Task.CompletedTask;
+                SessionMessageType.Play => SendPlayCommand((data as PlayRequest)!, cancellationToken),
+                SessionMessageType.Playstate => SendPlaystateCommand((data as PlaystateRequest)!, cancellationToken),
+                SessionMessageType.GeneralCommand => SendGeneralCommand((data as GeneralCommand)!, cancellationToken),
+                _ => Task.CompletedTask // Not supported or needed right now
+            };
         }
 
         private class StreamParams
         {
-            private MediaSourceInfo _mediaSource;
-            private IMediaSourceManager _mediaSourceManager;
+            private MediaSourceInfo? _mediaSource;
+            private IMediaSourceManager? _mediaSourceManager;
 
             public Guid ItemId { get; set; }
 
@@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
 
             public int? SubtitleStreamIndex { get; set; }
 
-            public string DeviceProfileId { get; set; }
+            public string? DeviceProfileId { get; set; }
 
-            public string DeviceId { get; set; }
+            public string? DeviceId { get; set; }
 
-            public string MediaSourceId { get; set; }
+            public string? MediaSourceId { get; set; }
 
-            public string LiveStreamId { get; set; }
+            public string? LiveStreamId { get; set; }
 
-            public BaseItem Item { get; set; }
+            public BaseItem? Item { get; set; }
 
-            public async Task<MediaSourceInfo> GetMediaSource(CancellationToken cancellationToken)
+            public async Task<MediaSourceInfo?> GetMediaSource(CancellationToken cancellationToken)
             {
                 if (_mediaSource is not null)
                 {
@@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
                 {
                     var part = parts[i];
 
-                    if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase) ||
-                        string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
+                    if (string.Equals(part, "audio", StringComparison.OrdinalIgnoreCase)
+                        || string.Equals(part, "videos", StringComparison.OrdinalIgnoreCase))
                     {
                         if (Guid.TryParse(parts[i + 1], out var result))
                         {

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

@@ -205,12 +205,11 @@ namespace Emby.Dlna.PlayTo
                     _userDataManager,
                     _localization,
                     _mediaSourceManager,
-                    _mediaEncoder);
+                    _mediaEncoder,
+                    device);
 
                 sessionInfo.AddController(controller);
 
-                controller.Init(device);
-
                 var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
                               _dlnaManager.GetDefaultProfile();
 

+ 6 - 6
Emby.Dlna/PlayTo/TransportCommands.cs

@@ -116,7 +116,7 @@ namespace Emby.Dlna.PlayTo
             return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
         }
 
-        public string BuildPost(ServiceAction action, string xmlNamesapce, object value, string commandParameter = "")
+        public string BuildPost(ServiceAction action, string xmlNamespace, object value, string commandParameter = "")
         {
             var stateString = string.Empty;
 
@@ -137,10 +137,10 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
         }
 
-        public string BuildPost(ServiceAction action, string xmlNamesapce, object value, Dictionary<string, string> dictionary)
+        public string BuildPost(ServiceAction action, string xmlNamespace, object value, Dictionary<string, string> dictionary)
         {
             var stateString = string.Empty;
 
@@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
                 {
                     stateString += BuildArgumentXml(arg, "0");
                 }
-                else if (dictionary.ContainsKey(arg.Name))
+                else if (dictionary.TryGetValue(arg.Name, out var argValue))
                 {
-                    stateString += BuildArgumentXml(arg, dictionary[arg.Name]);
+                    stateString += BuildArgumentXml(arg, argValue);
                 }
                 else
                 {
@@ -160,7 +160,7 @@ namespace Emby.Dlna.PlayTo
                 }
             }
 
-            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamesapce, stateString);
+            return string.Format(CultureInfo.InvariantCulture, CommandBase, action.Name, xmlNamespace, stateString);
         }
 
         private string BuildArgumentXml(Argument argument, string? value, string commandParameter = "")

+ 18 - 8
Emby.Dlna/Server/DescriptionXmlBuilder.cs

@@ -147,11 +147,16 @@ namespace Emby.Dlna.Server
             }
         }
 
-        private string GetFriendlyName()
+        internal string GetFriendlyName()
         {
             if (string.IsNullOrEmpty(_profile.FriendlyName))
             {
-                return "Jellyfin - " + _serverName;
+                return _serverName;
+            }
+
+            if (!_profile.FriendlyName.Contains("${HostName}", StringComparison.OrdinalIgnoreCase))
+            {
+                return _profile.FriendlyName;
             }
 
             var characterList = new List<char>();
@@ -164,13 +169,18 @@ namespace Emby.Dlna.Server
                 }
             }
 
-            var characters = characterList.ToArray();
-
-            var serverName = new string(characters);
-
-            var name = _profile.FriendlyName?.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
+            var serverName = string.Create(
+                characterList.Count,
+                characterList,
+                (dest, source) =>
+                {
+                    for (int i = 0; i < dest.Length; i++)
+                    {
+                        dest[i] = source[i];
+                    }
+                });
 
-            return name ?? string.Empty;
+            return _profile.FriendlyName.Replace("${HostName}", serverName, StringComparison.OrdinalIgnoreCase);
         }
 
         private void AppendIconList(StringBuilder builder)

+ 2 - 7
Emby.Naming/Audio/AlbumParser.cs

@@ -3,6 +3,7 @@ using System.Globalization;
 using System.IO;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 namespace Emby.Naming.Audio
 {
@@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
 
                 var tmp = trimmedFilename.Slice(prefix.Length).Trim();
 
-                int index = tmp.IndexOf(' ');
-                if (index != -1)
-                {
-                    tmp = tmp.Slice(0, index);
-                }
-
-                if (int.TryParse(tmp, NumberStyles.Integer, CultureInfo.InvariantCulture, out _))
+                if (int.TryParse(tmp.LeftPart(' '), CultureInfo.InvariantCulture, out _))
                 {
                     return true;
                 }

+ 3 - 3
Emby.Naming/AudioBook/AudioBookFilePathParser.cs

@@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
             var fileName = Path.GetFileNameWithoutExtension(path);
             foreach (var expression in _options.AudioBookPartsExpressions)
             {
-                var match = new Regex(expression, RegexOptions.IgnoreCase).Match(fileName);
+                var match = Regex.Match(fileName, expression, RegexOptions.IgnoreCase);
                 if (match.Success)
                 {
                     if (!result.ChapterNumber.HasValue)
@@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["chapter"];
                         if (value.Success)
                         {
-                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
                             {
                                 result.ChapterNumber = intValue;
                             }
@@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["part"];
                         if (value.Success)
                         {
-                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
                             {
                                 result.PartNumber = intValue;
                             }

+ 10 - 10
Emby.Naming/AudioBook/AudioBookListResolver.cs

@@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
                 {
                     if (group.Count() > 1 || haveChaptersOrPages)
                     {
-                        var ex = new List<AudioBookFileInfo>();
-                        var alt = new List<AudioBookFileInfo>();
+                        List<AudioBookFileInfo>? ex = null;
+                        List<AudioBookFileInfo>? alt = null;
 
                         foreach (var audioFile in group)
                         {
-                            var name = Path.GetFileNameWithoutExtension(audioFile.Path);
-                            if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase) ||
-                                name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase) ||
-                                name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
+                            var name = Path.GetFileNameWithoutExtension(audioFile.Path.AsSpan());
+                            if (name.Equals("audiobook", StringComparison.OrdinalIgnoreCase)
+                                || name.Contains(nameParserResult.Name, StringComparison.OrdinalIgnoreCase)
+                                || name.Contains(nameWithReplacedDots, StringComparison.OrdinalIgnoreCase))
                             {
-                                alt.Add(audioFile);
+                                (alt ??= new()).Add(audioFile);
                             }
                             else
                             {
-                                ex.Add(audioFile);
+                                (ex ??= new()).Add(audioFile);
                             }
                         }
 
-                        if (ex.Count > 0)
+                        if (ex is not null)
                         {
                             var extra = ex
                                 .OrderBy(x => x.Container)
@@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
                             extras.AddRange(extra);
                         }
 
-                        if (alt.Count > 0)
+                        if (alt is not null)
                         {
                             var alternatives = alt
                                 .OrderBy(x => x.Container)

+ 2 - 2
Emby.Naming/AudioBook/AudioBookNameParser.cs

@@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
             AudioBookNameParserResult result = default;
             foreach (var expression in _options.AudioBookNamesExpressions)
             {
-                var match = new Regex(expression, RegexOptions.IgnoreCase).Match(name);
+                var match = Regex.Match(name, expression, RegexOptions.IgnoreCase);
                 if (match.Success)
                 {
                     if (result.Name is null)
@@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["year"];
                         if (value.Success)
                         {
-                            if (int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
+                            if (int.TryParse(value.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue))
                             {
                                 result.Year = intValue;
                             }

+ 12 - 37
Emby.Naming/Common/NamingOptions.cs

@@ -141,8 +141,7 @@ namespace Emby.Naming.Common
             VideoFileStackingRules = new[]
             {
                 new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[0-9]+)[\)\]]?(?:\.[^.]+)?$", true),
-                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false),
-                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]?)(?<number>[a-d])(?:\.[^.]+)?$", false)
+                new FileStackRule(@"^(?<filename>.*?)(?:(?<=[\]\)\}])|[ _.-]+)[\(\[]?(?<parttype>cd|dvd|part|pt|dis[ck])[ _.-]*(?<number>[a-d])[\)\]]?(?:\.[^.]+)?$", false)
             };
 
             CleanDateTimes = new[]
@@ -157,7 +156,8 @@ namespace Emby.Naming.Common
                 @"^(?<cleaned>.+?)(\[.*\])",
                 @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
                 @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
-                @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$"
+                @"^\s*(?<cleaned>.+?)\s+-\s+[0-9]+\s*$",
+                @"^\s*(?<cleaned>.+?)(([-._ ](trailer|sample))|-(scene|clip|behindthescenes|deleted|deletedscene|featurette|short|interview|other|extra))$"
             };
 
             SubtitleFileExtensions = new[]
@@ -270,7 +270,6 @@ namespace Emby.Naming.Common
                 ".sfx",
                 ".shn",
                 ".sid",
-                ".spc",
                 ".stm",
                 ".strm",
                 ".ult",
@@ -338,7 +337,15 @@ namespace Emby.Naming.Common
                     }
                 },
 
-                // This isn't a Kodi naming rule, but the expression below causes false positives,
+                // This isn't a Kodi naming rule, but the expression below causes false episode numbers for
+                // Title Season X Episode X naming schemes.
+                // "Series Season X Episode X - Title.avi", "Series S03 E09.avi", "s3 e9 - Title.avi"
+                new EpisodeExpression(@".*[\\\/]((?<seriesname>[^\\/]+?)\s)?[Ss](?:eason)?\s*(?<seasonnumber>[0-9]+)\s+[Ee](?:pisode)?\s*(?<epnumber>[0-9]+).*$")
+                {
+                    IsNamed = true
+                },
+
+                // Not a Kodi rule as well, but the expression below also causes false positives,
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
@@ -453,16 +460,6 @@ namespace Emby.Naming.Common
                 },
             };
 
-            EpisodeWithoutSeasonExpressions = new[]
-            {
-                @"[/\._ \-]()([0-9]+)(-[0-9]+)?"
-            };
-
-            EpisodeMultiPartExpressions = new[]
-            {
-                @"^[-_ex]+([0-9]+(?:(?:[a-i]|\\.[1-9])(?![0-9]))?)"
-            };
-
             VideoExtraRules = new[]
             {
                 new ExtraRule(
@@ -797,16 +794,6 @@ namespace Emby.Naming.Common
         /// </summary>
         public EpisodeExpression[] EpisodeExpressions { get; set; }
 
-        /// <summary>
-        /// Gets or sets list of raw episode without season regular expressions strings.
-        /// </summary>
-        public string[] EpisodeWithoutSeasonExpressions { get; set; }
-
-        /// <summary>
-        /// Gets or sets list of raw multi-part episodes regular expressions strings.
-        /// </summary>
-        public string[] EpisodeMultiPartExpressions { get; set; }
-
         /// <summary>
         /// Gets or sets list of video file extensions.
         /// </summary>
@@ -877,16 +864,6 @@ namespace Emby.Naming.Common
         /// </summary>
         public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
 
-        /// <summary>
-        /// Gets list of episode without season regular expressions.
-        /// </summary>
-        public Regex[] EpisodeWithoutSeasonRegexes { get; private set; } = Array.Empty<Regex>();
-
-        /// <summary>
-        /// Gets list of multi-part episode regular expressions.
-        /// </summary>
-        public Regex[] EpisodeMultiPartRegexes { get; private set; } = Array.Empty<Regex>();
-
         /// <summary>
         /// Compiles raw regex strings into regexes.
         /// </summary>
@@ -894,8 +871,6 @@ namespace Emby.Naming.Common
         {
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
-            EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
-            EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
         }
 
         private Regex Compile(string exp)

+ 5 - 5
Emby.Naming/Emby.Naming.csproj

@@ -42,18 +42,18 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
   </ItemGroup>
 
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
 
 </Project>

+ 7 - 7
Emby.Naming/TV/EpisodePathParser.cs

@@ -113,7 +113,7 @@ namespace Emby.Naming.TV
                     if (expression.DateTimeFormats.Length > 0)
                     {
                         if (DateTime.TryParseExact(
-                            match.Groups[0].Value,
+                            match.Groups[0].ValueSpan,
                             expression.DateTimeFormats,
                             CultureInfo.InvariantCulture,
                             DateTimeStyles.None,
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
                             result.Success = true;
                         }
                     }
-                    else if (DateTime.TryParse(match.Groups[0].Value, out date))
+                    else if (DateTime.TryParse(match.Groups[0].ValueSpan, out date))
                     {
                         result.Year = date.Year;
                         result.Month = date.Month;
@@ -138,12 +138,12 @@ namespace Emby.Naming.TV
                 }
                 else if (expression.IsNamed)
                 {
-                    if (int.TryParse(match.Groups["seasonnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+                    if (int.TryParse(match.Groups["seasonnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
                     {
                         result.SeasonNumber = num;
                     }
 
-                    if (int.TryParse(match.Groups["epnumber"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+                    if (int.TryParse(match.Groups["epnumber"].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
                     {
                         result.EpisodeNumber = num;
                     }
@@ -158,7 +158,7 @@ namespace Emby.Naming.TV
                         if (nextIndex >= name.Length
                             || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
                         {
-                            if (int.TryParse(endingNumberGroup.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+                            if (int.TryParse(endingNumberGroup.ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
                             {
                                 result.EndingEpisodeNumber = num;
                             }
@@ -170,12 +170,12 @@ namespace Emby.Naming.TV
                 }
                 else
                 {
-                    if (int.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
+                    if (int.TryParse(match.Groups[1].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
                     {
                         result.SeasonNumber = num;
                     }
 
-                    if (int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
+                    if (int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out num))
                     {
                         result.EpisodeNumber = num;
                     }

+ 1 - 1
Emby.Naming/TV/SeriesResolver.cs

@@ -14,7 +14,7 @@ namespace Emby.Naming.TV
         /// Used for removing separators between words, i.e turns "The_show" into "The show" while
         /// preserving namings like "S.H.O.W".
         /// </summary>
-        private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
+        private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
 
         /// <summary>
         /// Resolve information about series from path.

+ 1 - 1
Emby.Naming/Video/CleanDateTimeParser.cs

@@ -43,7 +43,7 @@ namespace Emby.Naming.Video
                 && match.Groups.Count == 5
                 && match.Groups[1].Success
                 && match.Groups[2].Success
-                && int.TryParse(match.Groups[2].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
+                && int.TryParse(match.Groups[2].ValueSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
             {
                 result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
                 return true;

+ 1 - 1
Emby.Naming/Video/ExtraRuleResolver.cs

@@ -56,7 +56,7 @@ namespace Emby.Naming.Video
                 }
                 else if (rule.RuleType == ExtraRuleType.Regex)
                 {
-                    var filename = Path.GetFileName(path);
+                    var filename = Path.GetFileName(path.AsSpan());
 
                     var isMatch = Regex.IsMatch(filename, rule.Token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
 

+ 1 - 1
Emby.Naming/Video/FileStackRule.cs

@@ -17,7 +17,7 @@ public class FileStackRule
     /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
     public FileStackRule(string token, bool isNumerical)
     {
-        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
         IsNumerical = isNumerical;
     }
 

+ 36 - 20
Emby.Naming/Video/VideoListResolver.cs

@@ -4,6 +4,7 @@ using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 using MediaBrowser.Model.IO;
 
 namespace Emby.Naming.Video
@@ -13,6 +14,8 @@ namespace Emby.Naming.Video
     /// </summary>
     public static class VideoListResolver
     {
+        private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
         /// </summary>
@@ -106,6 +109,7 @@ namespace Emby.Naming.Video
             }
 
             // Cannot use Span inside local functions and delegates thus we cannot use LINQ here nor merge with the above [if]
+            VideoInfo? primary = null;
             for (var i = 0; i < videos.Count; i++)
             {
                 var video = videos[i];
@@ -114,29 +118,43 @@ namespace Emby.Naming.Video
                     continue;
                 }
 
-                if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
+                if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
                 {
                     return videos;
                 }
+
+                if (folderName.Equals(video.Files[0].FileNameWithoutExtension, StringComparison.Ordinal))
+                {
+                    primary = video;
+                }
+            }
+
+            if (videos.Count > 1)
+            {
+                var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+                videos.Clear();
+                foreach (var group in groups)
+                {
+                    if (group.Key)
+                    {
+                        videos.InsertRange(0, group.OrderByDescending(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+                    }
+                    else
+                    {
+                        videos.AddRange(group.OrderBy(x => x.Files[0].FileNameWithoutExtension.ToString(), new AlphanumericComparator()));
+                    }
+                }
             }
 
-            // The list is created and overwritten in the caller, so we are allowed to do in-place sorting
-            videos.Sort((x, y) => string.Compare(x.Name, y.Name, StringComparison.Ordinal));
+            primary ??= videos[0];
+            videos.Remove(primary);
 
             var list = new List<VideoInfo>
             {
-                videos[0]
+                primary
             };
 
-            var alternateVersionsLen = videos.Count - 1;
-            var alternateVersions = new VideoFileInfo[alternateVersionsLen];
-            for (int i = 0; i < alternateVersionsLen; i++)
-            {
-                var video = videos[i + 1];
-                alternateVersions[i] = video.Files[0];
-            }
-
-            list[0].AlternateVersions = alternateVersions;
+            list[0].AlternateVersions = videos.Select(x => x.Files[0]).ToArray();
             list[0].Name = folderName.ToString();
 
             return list;
@@ -161,9 +179,8 @@ namespace Emby.Naming.Video
             return true;
         }
 
-        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, string testFilePath, NamingOptions namingOptions)
+        private static bool IsEligibleForMultiVersion(ReadOnlySpan<char> folderName, ReadOnlySpan<char> testFilename, NamingOptions namingOptions)
         {
-            var testFilename = Path.GetFileNameWithoutExtension(testFilePath.AsSpan());
             if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             {
                 return false;
@@ -176,16 +193,15 @@ namespace Emby.Naming.Video
             }
 
             // There are no span overloads for regex unfortunately
-            var tmpTestFilename = testFilename.ToString();
-            if (CleanStringParser.TryClean(tmpTestFilename, namingOptions.CleanStringRegexes, out var cleanName))
+            if (CleanStringParser.TryClean(testFilename.ToString(), namingOptions.CleanStringRegexes, out var cleanName))
             {
-                tmpTestFilename = cleanName.Trim();
+                testFilename = cleanName.AsSpan().Trim();
             }
 
             // The CleanStringParser should have removed common keywords etc.
-            return string.IsNullOrEmpty(tmpTestFilename)
+            return testFilename.IsEmpty
                    || testFilename[0] == '-'
-                   || Regex.IsMatch(tmpTestFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+                   || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
         }
     }
 }

+ 1 - 2
Emby.Naming/Video/VideoResolver.cs

@@ -87,8 +87,7 @@ namespace Emby.Naming.Video
                 name = cleanDateTimeResult.Name;
                 year = cleanDateTimeResult.Year;
 
-                if (extraResult.ExtraType is null
-                    && TryCleanString(name, namingOptions, out var newName))
+                if (TryCleanString(name, namingOptions, out var newName))
                 {
                     name = newName;
                 }

+ 5 - 5
Emby.Photos/Emby.Photos.csproj

@@ -15,7 +15,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="TagLibSharp" Version="2.3.0" />
+    <PackageReference Include="TagLibSharp" />
   </ItemGroup>
 
   <PropertyGroup>
@@ -26,13 +26,13 @@
 
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
-    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
 
 </Project>

+ 17 - 35
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
@@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
 
-        /// <summary>
-        /// The _configuration loaded.
-        /// </summary>
-        private bool _configurationLoaded;
-
         /// <summary>
         /// The _configuration.
         /// </summary>
-        private BaseApplicationConfiguration _configuration;
+        private BaseApplicationConfiguration? _configuration;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
         /// <summary>
         /// Occurs when [configuration updated].
         /// </summary>
-        public event EventHandler<EventArgs> ConfigurationUpdated;
+        public event EventHandler<EventArgs>? ConfigurationUpdated;
 
         /// <summary>
         /// Occurs when [configuration updating].
         /// </summary>
-        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
 
         /// <summary>
         /// Occurs when [named configuration updated].
         /// </summary>
-        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
 
         /// <summary>
         /// Gets the type of the configuration.
@@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
         {
             get
             {
-                if (_configurationLoaded)
+                if (_configuration is not null)
                 {
                     return _configuration;
                 }
 
                 lock (_configurationSyncLock)
                 {
-                    if (_configurationLoaded)
+                    if (_configuration is not null)
                     {
                         return _configuration;
                     }
 
-                    _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
-
-                    _configurationLoaded = true;
-
-                    return _configuration;
+                    return _configuration = (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer);
                 }
             }
 
             protected set
             {
                 _configuration = value;
-
-                _configurationLoaded = value is not null;
             }
         }
 
@@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
             Logger.LogInformation("Saving system configuration");
             var path = CommonApplicationPaths.SystemConfigurationFilePath;
 
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
 
             lock (_configurationSyncLock)
             {
@@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
 
         private object LoadConfiguration(string path, Type configurationType)
         {
-            if (!File.Exists(path))
-            {
-                return Activator.CreateInstance(configurationType);
-            }
-
             try
             {
-                return XmlSerializer.DeserializeFromFile(configurationType, path);
-            }
-            catch (IOException)
-            {
-                return Activator.CreateInstance(configurationType);
+                if (File.Exists(path))
+                {
+                    return XmlSerializer.DeserializeFromFile(configurationType, path);
+                }
             }
-            catch (Exception ex)
+            catch (Exception ex) when (ex is not IOException)
             {
                 Logger.LogError(ex, "Error loading configuration file: {Path}", path);
-
-                return Activator.CreateInstance(configurationType);
             }
+
+            return Activator.CreateInstance(configurationType)
+                ?? throw new InvalidOperationException("Configuration type can't be Nullable<T>.");
         }
 
         /// <inheritdoc />
@@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
             _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
 
             var path = GetConfigurationFile(key);
-            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Path can't be a root directory."));
 
             lock (_configurationSyncLock)
             {

+ 20 - 77
Emby.Server.Implementations/ApplicationHost.cs

@@ -11,7 +11,6 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Reflection;
-using System.Runtime.InteropServices;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading.Tasks;
@@ -81,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.System;
@@ -113,15 +114,11 @@ namespace Emby.Server.Implementations
     /// </summary>
     public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
     {
-        /// <summary>
-        /// The environment variable prefixes to log at server startup.
-        /// </summary>
-        private static readonly string[] _relevantEnvVarPrefixes = { "JELLYFIN_", "DOTNET_", "ASPNETCORE_" };
-
         /// <summary>
         /// The disposable parts.
         /// </summary>
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
+        private readonly DeviceId _deviceId;
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IConfiguration _startupConfig;
@@ -130,7 +127,6 @@ namespace Emby.Server.Implementations
         private readonly IPluginManager _pluginManager;
 
         private List<Type> _creatingInstances;
-        private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
 
         /// <summary>
@@ -139,8 +135,6 @@ namespace Emby.Server.Implementations
         /// <value>All concrete types.</value>
         private Type[] _allConcreteTypes;
 
-        private DeviceId _deviceId;
-
         private bool _disposed = false;
 
         /// <summary>
@@ -164,6 +158,7 @@ namespace Emby.Server.Implementations
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+            _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
 
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
             ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -191,23 +186,9 @@ namespace Emby.Server.Implementations
 
         public bool CoreStartupHasCompleted { get; private set; }
 
-        public virtual bool CanLaunchWebBrowser
-        {
-            get
-            {
-                if (!Environment.UserInteractive)
-                {
-                    return false;
-                }
-
-                if (_startupOptions.IsService)
-                {
-                    return false;
-                }
-
-                return OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
-            }
-        }
+        public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
+            && !_startupOptions.IsService
+            && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
 
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
@@ -284,15 +265,7 @@ namespace Emby.Server.Implementations
         /// <value>The application name.</value>
         public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
 
-        public string SystemId
-        {
-            get
-            {
-                _deviceId ??= new DeviceId(ApplicationPaths, LoggerFactory);
-
-                return _deviceId.Value;
-            }
-        }
+        public string SystemId => _deviceId.Value;
 
         /// <inheritdoc/>
         public string Name => ApplicationProductName;
@@ -445,7 +418,7 @@ namespace Emby.Server.Implementations
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
             ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
 
-            _mediaEncoder.SetFFmpegPath();
+            Resolve<IMediaEncoder>().SetFFmpegPath();
 
             Logger.LogInformation("ServerId: {ServerId}", SystemId);
 
@@ -558,6 +531,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
+            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
@@ -652,50 +627,19 @@ namespace Emby.Server.Implementations
                 }
             }
 
+            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize();
+            ((SqliteUserDataRepository)Resolve<IUserDataRepository>()).Initialize();
+
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 
-            _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
 
             SetStaticProperties();
 
-            var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
-
             FindParts();
         }
 
-        public static void LogEnvironmentInfo(ILogger logger, IApplicationPaths appPaths)
-        {
-            // Distinct these to prevent users from reporting problems that aren't actually problems
-            var commandLineArgs = Environment
-                .GetCommandLineArgs()
-                .Distinct();
-
-            // Get all relevant environment variables
-            var allEnvVars = Environment.GetEnvironmentVariables();
-            var relevantEnvVars = new Dictionary<object, object>();
-            foreach (var key in allEnvVars.Keys)
-            {
-                if (_relevantEnvVarPrefixes.Any(prefix => key.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase)))
-                {
-                    relevantEnvVars.Add(key, allEnvVars[key]);
-                }
-            }
-
-            logger.LogInformation("Environment Variables: {EnvVars}", relevantEnvVars);
-            logger.LogInformation("Arguments: {Args}", commandLineArgs);
-            logger.LogInformation("Operating system: {OS}", RuntimeInformation.OSDescription);
-            logger.LogInformation("Architecture: {Architecture}", RuntimeInformation.OSArchitecture);
-            logger.LogInformation("64-Bit Process: {Is64Bit}", Environment.Is64BitProcess);
-            logger.LogInformation("User Interactive: {IsUserInteractive}", Environment.UserInteractive);
-            logger.LogInformation("Processor count: {ProcessorCount}", Environment.ProcessorCount);
-            logger.LogInformation("Program data path: {ProgramDataPath}", appPaths.ProgramDataPath);
-            logger.LogInformation("Web resources path: {WebPath}", appPaths.WebPath);
-            logger.LogInformation("Application directory: {ApplicationPath}", appPaths.ProgramSystemPath);
-        }
-
         private X509Certificate2 GetCertificate(string path, string password)
         {
             if (string.IsNullOrWhiteSpace(path))
@@ -782,10 +726,6 @@ namespace Emby.Server.Implementations
 
             Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
-            Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
-
-            Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
-
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
         }
 
@@ -1248,10 +1188,13 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            // used for closing websockets
-            foreach (var session in _sessionManager.Sessions)
+            if (_sessionManager != null)
             {
-                await session.DisposeAsync().ConfigureAwait(false);
+                // used for closing websockets
+                foreach (var session in _sessionManager.Sessions)
+                {
+                    await session.DisposeAsync().ConfigureAwait(false);
+                }
             }
         }
     }

+ 20 - 19
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -66,6 +66,7 @@ namespace Emby.Server.Implementations.Channels
         /// <param name="userDataManager">The user data manager.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="memoryCache">The memory cache.</param>
+        /// <param name="channels">The channels.</param>
         public ChannelManager(
             IUserManager userManager,
             IDtoService dtoService,
@@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
             IProviderManager providerManager,
-            IMemoryCache memoryCache)
+            IMemoryCache memoryCache,
+            IEnumerable<IChannel> channels)
         {
             _userManager = userManager;
             _dtoService = dtoService;
@@ -86,18 +88,13 @@ namespace Emby.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _providerManager = providerManager;
             _memoryCache = memoryCache;
+            Channels = channels.ToArray();
         }
 
-        internal IChannel[] Channels { get; private set; }
+        internal IChannel[] Channels { get; }
 
         private static TimeSpan CacheLength => TimeSpan.FromHours(3);
 
-        /// <inheritdoc />
-        public void AddParts(IEnumerable<IChannel> channels)
-        {
-            Channels = channels.ToArray();
-        }
-
         /// <inheritdoc />
         public bool EnableMediaSourceDisplay(BaseItem item)
         {
@@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
         }
 
         /// <inheritdoc />
-        public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
+        public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
         {
             var user = query.UserId.Equals(default)
                 ? null
                 : _userManager.GetUserById(query.UserId);
 
-            var channels = GetAllChannels()
-                .Select(GetChannelEntity)
+            var channels = await GetAllChannelEntitiesAsync()
                 .OrderBy(i => i.SortName)
-                .ToList();
+                .ToListAsync()
+                .ConfigureAwait(false);
 
             if (query.IsRecordingsFolder.HasValue)
             {
@@ -229,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
 
             if (user is not null)
             {
+                var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
                 channels = channels.Where(i =>
                 {
                     if (!i.IsVisible(user))
@@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
 
                     try
                     {
-                        return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
+                        return GetChannelProvider(i).IsEnabledFor(userId);
                     }
                     catch
                     {
@@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
             {
                 foreach (var item in all)
                 {
-                    RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).GetAwaiter().GetResult();
+                    await RefreshLatestChannelItems(GetChannelProvider(item), CancellationToken.None).ConfigureAwait(false);
                 }
             }
 
@@ -272,13 +270,13 @@ namespace Emby.Server.Implementations.Channels
         }
 
         /// <inheritdoc />
-        public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
+        public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
         {
             var user = query.UserId.Equals(default)
                 ? null
                 : _userManager.GetUserById(query.UserId);
 
-            var internalResult = GetChannelsInternal(query);
+            var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
 
             var dtoOptions = new DtoOptions();
 
@@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
             progress.Report(100);
         }
 
-        private Channel GetChannelEntity(IChannel channel)
+        private async IAsyncEnumerable<Channel> GetAllChannelEntitiesAsync()
         {
-            return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult();
+            foreach (IChannel channel in GetAllChannels())
+            {
+                yield return GetChannel(GetInternalChannelId(channel.Name)) ?? await GetChannel(channel, CancellationToken.None).ConfigureAwait(false);
+            }
         }
 
         private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
@@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
             }
             else
             {
-                results = new List<MediaSourceInfo>();
+                results = Enumerable.Empty<MediaSourceInfo>();
             }
 
             return results

+ 13 - 8
Emby.Server.Implementations/Collections/CollectionManager.cs

@@ -112,7 +112,8 @@ namespace Emby.Server.Implementations.Collections
             return Path.Combine(_appPaths.DataPath, "collections");
         }
 
-        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
+        /// <inheritdoc />
+        public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }
@@ -206,8 +207,7 @@ namespace Emby.Server.Implementations.Collections
                 throw new ArgumentException("No collection exists with the supplied Id");
             }
 
-            var list = new List<LinkedChild>();
-            var itemList = new List<BaseItem>();
+            List<BaseItem>? itemList = null;
 
             var linkedChildrenList = collection.GetLinkedChildren();
             var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
@@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
 
                 if (!currentLinkedChildrenIds.Contains(id))
                 {
-                    itemList.Add(item);
+                    (itemList ??= new()).Add(item);
 
-                    list.Add(LinkedChild.Create(item));
                     linkedChildrenList.Add(item);
                 }
             }
 
-            if (list.Count > 0)
+            if (itemList is not null)
             {
-                LinkedChild[] newChildren = new LinkedChild[collection.LinkedChildren.Length + list.Count];
+                var originalLen = collection.LinkedChildren.Length;
+                var newItemCount = itemList.Count;
+                LinkedChild[] newChildren = new LinkedChild[originalLen + newItemCount];
                 collection.LinkedChildren.CopyTo(newChildren, 0);
-                list.CopyTo(newChildren, collection.LinkedChildren.Length);
+                for (int i = 0; i < newItemCount; i++)
+                {
+                    newChildren[originalLen + i] = LinkedChild.Create(itemList[i]);
+                }
+
                 collection.LinkedChildren = newChildren;
                 collection.UpdateRatingToItems(linkedChildrenList);
 

+ 1 - 3
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System.Globalization;
 using System.IO;
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.Configuration
         /// <summary>
         /// Configuration updating event.
         /// </summary>
-        public event EventHandler<GenericEventArgs<ServerConfiguration>> ConfigurationUpdating;
+        public event EventHandler<GenericEventArgs<ServerConfiguration>>? ConfigurationUpdating;
 
         /// <summary>
         /// Gets the type of the configuration.

+ 4 - 3
Emby.Server.Implementations/ConfigurationOptions.cs

@@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Gets a new copy of the default configuration options.
         /// </summary>
-        public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
+        public static Dictionary<string, string?> DefaultConfiguration => new()
         {
             { HostWebClientKey, bool.TrueString },
-            { DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.FalseString },
-            { BindToUnixSocketKey, bool.FalseString }
+            { BindToUnixSocketKey, bool.FalseString },
+            { SqliteCacheSizeKey, "20000" }
         };
     }
 }

+ 77 - 48
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -4,7 +4,6 @@
 
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
@@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// Gets or sets the path to the DB file.
         /// </summary>
-        /// <value>Path to the DB file.</value>
         protected string DbFilePath { get; set; }
 
+        /// <summary>
+        /// Gets or sets the number of write connections to create.
+        /// </summary>
+        /// <value>Path to the DB file.</value>
+        protected int WriteConnectionsCount { get; set; } = 1;
+
+        /// <summary>
+        /// Gets or sets the number of read connections to create.
+        /// </summary>
+        protected int ReadConnectionsCount { get; set; } = 1;
+
         /// <summary>
         /// Gets the logger.
         /// </summary>
@@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
         /// </summary>
-        protected virtual string LockingMode => "EXCLUSIVE";
+        protected virtual string LockingMode => "NORMAL";
 
         /// <summary>
         /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
@@ -73,9 +82,10 @@ namespace Emby.Server.Implementations.Data
 
         /// <summary>
         /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
+        /// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
         /// </summary>
         /// <value>The journal size limit.</value>
-        protected virtual int? JournalSizeLimit => 0;
+        protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
 
         /// <summary>
         /// Gets the page size.
@@ -88,7 +98,7 @@ namespace Emby.Server.Implementations.Data
         /// </summary>
         /// <value>The temp store mode.</value>
         /// <see cref="TempStoreMode"/>
-        protected virtual TempStoreMode TempStore => TempStoreMode.Default;
+        protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
 
         /// <summary>
         /// Gets the synchronous mode.
@@ -101,83 +111,114 @@ namespace Emby.Server.Implementations.Data
         /// Gets or sets the write lock.
         /// </summary>
         /// <value>The write lock.</value>
-        protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
+        protected ConnectionPool WriteConnections { get; set; }
 
         /// <summary>
         /// Gets or sets the write connection.
         /// </summary>
         /// <value>The write connection.</value>
-        protected SQLiteDatabaseConnection WriteConnection { get; set; }
+        protected ConnectionPool ReadConnections { get; set; }
 
-        protected ManagedConnection GetConnection(bool readOnly = false)
+        public virtual void Initialize()
         {
-            WriteLock.Wait();
-            if (WriteConnection is not null)
+            WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
+            ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
+
+            // Configuration and pragmas can affect VACUUM so it needs to be last.
+            using (var connection = GetConnection())
             {
-                return new ManagedConnection(WriteConnection, WriteLock);
+                connection.Execute("VACUUM");
             }
+        }
+
+        protected ManagedConnection GetConnection(bool readOnly = false)
+            => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
 
-            WriteConnection = SQLite3.Open(
+        protected SQLiteDatabaseConnection CreateWriteConnection()
+        {
+            var writeConnection = SQLite3.Open(
                 DbFilePath,
                 DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
                 null);
 
             if (CacheSize.HasValue)
             {
-                WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
+                writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
             }
 
             if (!string.IsNullOrWhiteSpace(LockingMode))
             {
-                WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
+                writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
             }
 
             if (!string.IsNullOrWhiteSpace(JournalMode))
             {
-                WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
+                writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
             }
 
             if (JournalSizeLimit.HasValue)
             {
-                WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
+                writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
             }
 
             if (Synchronous.HasValue)
             {
-                WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
+                writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
             }
 
             if (PageSize.HasValue)
             {
-                WriteConnection.Execute("PRAGMA page_size=" + PageSize.Value);
+                writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
             }
 
-            WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
-            // Configuration and pragmas can affect VACUUM so it needs to be last.
-            WriteConnection.Execute("VACUUM");
+            writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
 
-            return new ManagedConnection(WriteConnection, WriteLock);
+            return writeConnection;
         }
 
-        public IStatement PrepareStatement(ManagedConnection connection, string sql)
-            => connection.PrepareStatement(sql);
+        protected SQLiteDatabaseConnection CreateReadConnection()
+        {
+            var connection = SQLite3.Open(
+                DbFilePath,
+                DefaultConnectionFlags | ConnectionFlags.ReadOnly,
+                null);
 
-        public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
-            => connection.PrepareStatement(sql);
+            if (CacheSize.HasValue)
+            {
+                connection.Execute("PRAGMA cache_size=" + CacheSize.Value);
+            }
 
-        public IStatement[] PrepareAll(IDatabaseConnection connection, IReadOnlyList<string> sql)
-        {
-            int len = sql.Count;
-            IStatement[] statements = new IStatement[len];
-            for (int i = 0; i < len; i++)
+            if (!string.IsNullOrWhiteSpace(LockingMode))
+            {
+                connection.Execute("PRAGMA locking_mode=" + LockingMode);
+            }
+
+            if (!string.IsNullOrWhiteSpace(JournalMode))
+            {
+                connection.Execute("PRAGMA journal_mode=" + JournalMode);
+            }
+
+            if (JournalSizeLimit.HasValue)
+            {
+                connection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
+            }
+
+            if (Synchronous.HasValue)
             {
-                statements[i] = connection.PrepareStatement(sql[i]);
+                connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
             }
 
-            return statements;
+            connection.Execute("PRAGMA temp_store=" + (int)TempStore);
+
+            return connection;
         }
 
+        public IStatement PrepareStatement(ManagedConnection connection, string sql)
+            => connection.PrepareStatement(sql);
+
+        public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
+            => connection.PrepareStatement(sql);
+
         protected bool TableExists(ManagedConnection connection, string name)
         {
             return connection.RunInTransaction(
@@ -252,22 +293,10 @@ namespace Emby.Server.Implementations.Data
 
             if (dispose)
             {
-                WriteLock.Wait();
-                try
-                {
-                    WriteConnection?.Dispose();
-                }
-                finally
-                {
-                    WriteLock.Release();
-                }
-
-                WriteLock.Dispose();
+                WriteConnections.Dispose();
+                ReadConnections.Dispose();
             }
 
-            WriteConnection = null;
-            WriteLock = null;
-
             _disposed = true;
         }
     }

+ 79 - 0
Emby.Server.Implementations/Data/ConnectionPool.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Concurrent;
+using SQLitePCL.pretty;
+
+namespace Emby.Server.Implementations.Data;
+
+/// <summary>
+/// A pool of SQLite Database connections.
+/// </summary>
+public sealed class ConnectionPool : IDisposable
+{
+    private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
+    private bool _disposed;
+
+    /// <summary>
+    /// Initializes a new instance of the <see cref="ConnectionPool" /> class.
+    /// </summary>
+    /// <param name="count">The number of database connection to create.</param>
+    /// <param name="factory">Factory function to create the database connections.</param>
+    public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
+    {
+        for (int i = 0; i < count; i++)
+        {
+            _connections.Add(factory.Invoke());
+        }
+    }
+
+    /// <summary>
+    /// Gets a database connection from the pool if one is available, otherwise blocks.
+    /// </summary>
+    /// <returns>A database connection.</returns>
+    public ManagedConnection GetConnection()
+    {
+        if (_disposed)
+        {
+            ThrowObjectDisposedException();
+        }
+
+        return new ManagedConnection(_connections.Take(), this);
+
+        static void ThrowObjectDisposedException()
+        {
+            throw new ObjectDisposedException(nameof(ConnectionPool));
+        }
+    }
+
+    /// <summary>
+    /// Return a database connection to the pool.
+    /// </summary>
+    /// <param name="connection">The database connection to return.</param>
+    public void Return(SQLiteDatabaseConnection connection)
+    {
+        if (_disposed)
+        {
+            connection.Dispose();
+            return;
+        }
+
+        _connections.Add(connection);
+    }
+
+    /// <inheritdoc />
+    public void Dispose()
+    {
+        if (_disposed)
+        {
+            return;
+        }
+
+        foreach (var connection in _connections)
+        {
+            connection.Dispose();
+        }
+
+        _connections.Dispose();
+
+        _disposed = true;
+    }
+}

+ 6 - 7
Emby.Server.Implementations/Data/ManagedConnection.cs

@@ -2,23 +2,22 @@
 
 using System;
 using System.Collections.Generic;
-using System.Threading;
 using SQLitePCL.pretty;
 
 namespace Emby.Server.Implementations.Data
 {
     public sealed class ManagedConnection : IDisposable
     {
-        private readonly SemaphoreSlim _writeLock;
+        private readonly ConnectionPool _pool;
 
-        private SQLiteDatabaseConnection? _db;
+        private SQLiteDatabaseConnection _db;
 
         private bool _disposed = false;
 
-        public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
+        public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
         {
             _db = db;
-            _writeLock = writeLock;
+            _pool = pool;
         }
 
         public IStatement PrepareStatement(string sql)
@@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
                 return;
             }
 
-            _writeLock.Release();
+            _pool.Return(_db);
 
-            _db = null; // Don't dispose it
+            _db = null!; // Don't dispose it
             _disposed = true;
         }
     }

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 226 - 256
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 11 - 27
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -7,7 +7,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Threading;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
@@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
 {
     public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
     {
+        private readonly IUserManager _userManager;
+
         public SqliteUserDataRepository(
             ILogger<SqliteUserDataRepository> logger,
-            IApplicationPaths appPaths)
+            IServerConfigurationManager config,
+            IUserManager userManager)
             : base(logger)
         {
-            DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
+            _userManager = userManager;
+
+            DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
         }
 
         /// <summary>
         /// Opens the connection to the database.
         /// </summary>
-        /// <param name="userManager">The user manager.</param>
-        /// <param name="dbLock">The lock to use for database IO.</param>
-        /// <param name="dbConnection">The connection to use for database IO.</param>
-        public void Initialize(IUserManager userManager, SemaphoreSlim dbLock, SQLiteDatabaseConnection dbConnection)
+        public override void Initialize()
         {
-            WriteLock.Dispose();
-            WriteLock = dbLock;
-            WriteConnection?.Dispose();
-            WriteConnection = dbConnection;
+            base.Initialize();
 
             using (var connection = GetConnection())
             {
                 var userDatasTableExists = TableExists(connection, "UserDatas");
                 var userDataTableExists = TableExists(connection, "userdata");
 
-                var users = userDatasTableExists ? null : userManager.Users;
+                var users = userDatasTableExists ? null : _userManager.Users;
 
                 connection.RunInTransaction(
                     db =>
@@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data
 
             return userData;
         }
-
-#pragma warning disable CA2215
-        /// <inheritdoc/>
-        /// <remarks>
-        /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and
-        /// <see cref="BaseSqliteRepository.WriteConnection"/> are managed by <see cref="SqliteItemRepository"/>.
-        /// See <see cref="Initialize(IUserManager, SemaphoreSlim, SQLiteDatabaseConnection)"/>.
-        /// </remarks>
-        protected override void Dispose(bool dispose)
-        {
-            // The write lock and connection for the item repository are shared with the user data repository
-            // since they point to the same database. The item repo has responsibility for disposing these two objects,
-            // so the user data repo should not attempt to dispose them as well
-        }
-#pragma warning restore CA2215
     }
 }

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

@@ -7,7 +7,6 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Linq;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
@@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto
         /// <inheritdoc />
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         {
-            var returnItems = new BaseItemDto[items.Count];
-            var programTuples = new List<(BaseItem, BaseItemDto)>();
-            var channelTuples = new List<(BaseItemDto, LiveTvChannel)>();
+            var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
+            var returnItems = new BaseItemDto[accessibleItems.Count];
+            List<(BaseItem, BaseItemDto)> programTuples = null;
+            List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
 
-            for (int index = 0; index < items.Count; index++)
+            for (int index = 0; index < accessibleItems.Count; index++)
             {
-                var item = items[index];
+                var item = accessibleItems[index];
                 var dto = GetBaseItemDtoInternal(item, options, user, owner);
 
                 if (item is LiveTvChannel tvChannel)
                 {
-                    channelTuples.Add((dto, tvChannel));
+                    (channelTuples ??= new()).Add((dto, tvChannel));
                 }
                 else if (item is LiveTvProgram)
                 {
-                    programTuples.Add((item, dto));
+                    (programTuples ??= new()).Add((item, dto));
                 }
 
                 if (item is IItemByName byName)
@@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto
                 returnItems[index] = dto;
             }
 
-            if (programTuples.Count > 0)
+            if (programTuples is not null)
             {
                 LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
             }
 
-            if (channelTuples.Count > 0)
+            if (channelTuples is not null)
             {
                 LivetvManager.AddChannelInfo(channelTuples, options, user);
             }
@@ -522,32 +522,32 @@ namespace Emby.Server.Implementations.Dto
             var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
                 .ThenBy(i =>
                 {
-                    if (i.IsType(PersonType.Actor))
+                    if (i.IsType(PersonKind.Actor))
                     {
                         return 0;
                     }
 
-                    if (i.IsType(PersonType.GuestStar))
+                    if (i.IsType(PersonKind.GuestStar))
                     {
                         return 1;
                     }
 
-                    if (i.IsType(PersonType.Director))
+                    if (i.IsType(PersonKind.Director))
                     {
                         return 2;
                     }
 
-                    if (i.IsType(PersonType.Writer))
+                    if (i.IsType(PersonKind.Writer))
                     {
                         return 3;
                     }
 
-                    if (i.IsType(PersonType.Producer))
+                    if (i.IsType(PersonKind.Producer))
                     {
                         return 4;
                     }
 
-                    if (i.IsType(PersonType.Composer))
+                    if (i.IsType(PersonKind.Composer))
                     {
                         return 4;
                     }
@@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
                         return null;
                     }
                 }).Where(i => i is not null)
-                .Where(i => user is null ?
-                    true :
-                    i.IsVisible(user))
+                .Where(i => user is null || i.IsVisible(user))
                 .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
 
@@ -908,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
             // Add audio info
             if (item is Audio audio)
             {
+                dto.LUFS = audio.LUFS;
                 dto.Album = audio.Album;
                 if (audio.ExtraType.HasValue)
                 {

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

@@ -22,17 +22,17 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
-    <PackageReference Include="Jellyfin.XmlTv" Version="10.8.0" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.2" />
-    <PackageReference Include="Mono.Nat" Version="3.0.4" />
-    <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
-    <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
-    <PackageReference Include="DotNet.Glob" Version="3.1.3" />
+    <PackageReference Include="DiscUtils.Udf" />
+    <PackageReference Include="Jellyfin.XmlTv" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
+    <PackageReference Include="Mono.Nat" />
+    <PackageReference Include="prometheus-net.DotNetRuntime" />
+    <PackageReference Include="SQLitePCL.pretty.netstandard" />
+    <PackageReference Include="DotNet.Glob" />
   </ItemGroup>
 
   <ItemGroup>
@@ -53,13 +53,13 @@
 
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" />
-    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
-    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" />
+    <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
+    <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
 
   <ItemGroup>

+ 24 - 17
Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs

@@ -12,6 +12,7 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints
 {
     public class LibraryChangedNotifier : IServerEntryPoint
     {
-        /// <summary>
-        /// The library update duration.
-        /// </summary>
-        private const int LibraryUpdateDuration = 30000;
-
         private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _configurationManager;
         private readonly IProviderManager _providerManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
@@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints
 
         public LibraryChangedNotifier(
             ILibraryManager libraryManager,
+            IServerConfigurationManager configurationManager,
             ISessionManager sessionManager,
             IUserManager userManager,
             ILogger<LibraryChangedNotifier> logger,
             IProviderManager providerManager)
         {
             _libraryManager = libraryManager;
+            _configurationManager = configurationManager;
             _sessionManager = sessionManager;
             _userManager = userManager;
             _logger = logger;
@@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer = new Timer(
                         LibraryUpdateTimerCallback,
                         null,
-                        LibraryUpdateDuration,
-                        Timeout.Infinite);
+                        TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
+                        Timeout.InfiniteTimeSpan);
                 }
                 else
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
 
                 if (e.Item.GetParent() is Folder parent)
@@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints
             {
                 if (LibraryUpdateTimer is null)
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
                 else
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
 
                 _itemsUpdated.Add(e.Item);
@@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints
             {
                 if (LibraryUpdateTimer is null)
                 {
-                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
                 else
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
 
                 if (e.Parent is Folder parent)
@@ -276,25 +275,31 @@ namespace Emby.Server.Implementations.EntryPoints
         /// Libraries the update timer callback.
         /// </summary>
         /// <param name="state">The state.</param>
-        private void LibraryUpdateTimerCallback(object state)
+        private async void LibraryUpdateTimerCallback(object state)
         {
+            List<Folder> foldersAddedTo;
+            List<Folder> foldersRemovedFrom;
+            List<BaseItem> itemsUpdated;
+            List<BaseItem> itemsAdded;
+            List<BaseItem> itemsRemoved;
             lock (_libraryChangedSyncLock)
             {
                 // Remove dupes in case some were saved multiple times
-                var foldersAddedTo = _foldersAddedTo
+                foldersAddedTo = _foldersAddedTo
                                         .DistinctBy(x => x.Id)
                                         .ToList();
 
-                var foldersRemovedFrom = _foldersRemovedFrom
+                foldersRemovedFrom = _foldersRemovedFrom
                                             .DistinctBy(x => x.Id)
                                             .ToList();
 
-                var itemsUpdated = _itemsUpdated
+                itemsUpdated = _itemsUpdated
                                     .Where(i => !_itemsAdded.Contains(i))
                                     .DistinctBy(x => x.Id)
                                     .ToList();
 
-                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
+                itemsAdded = _itemsAdded.ToList();
+                itemsRemoved = _itemsRemoved.ToList();
 
                 if (LibraryUpdateTimer is not null)
                 {
@@ -308,6 +313,8 @@ namespace Emby.Server.Implementations.EntryPoints
                 _foldersAddedTo.Clear();
                 _foldersRemovedFrom.Clear();
             }
+
+            await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
         }
 
         /// <summary>

+ 7 - 6
Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs

@@ -87,29 +87,30 @@ namespace Emby.Server.Implementations.EntryPoints
             }
         }
 
-        private void UpdateTimerCallback(object? state)
+        private async void UpdateTimerCallback(object? state)
         {
+            List<KeyValuePair<Guid, List<BaseItem>>> changes;
             lock (_syncLock)
             {
                 // Remove dupes in case some were saved multiple times
-                var changes = _changedItems.ToList();
+                changes = _changedItems.ToList();
                 _changedItems.Clear();
 
-                SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
-
                 if (_updateTimer is not null)
                 {
                     _updateTimer.Dispose();
                     _updateTimer = null;
                 }
             }
+
+            await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
         }
 
         private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
         {
-            foreach (var pair in changes)
+            foreach ((var key, var value) in changes)
             {
-                await SendNotifications(pair.Key, pair.Value, cancellationToken).ConfigureAwait(false);
+                await SendNotifications(key, value, cancellationToken).ConfigureAwait(false);
             }
         }
 

+ 14 - 2
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -9,7 +9,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
@@ -85,6 +85,18 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The state.</value>
         public WebSocketState State => _socket.State;
 
+        /// <summary>
+        /// Sends a message asynchronously.
+        /// </summary>
+        /// <param name="message">The message.</param>
+        /// <param name="cancellationToken">The cancellation token.</param>
+        /// <returns>Task.</returns>
+        public Task SendAsync(WebSocketMessage message, CancellationToken cancellationToken)
+        {
+            var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+            return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+        }
+
         /// <summary>
         /// Sends a message asynchronously.
         /// </summary>
@@ -224,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer
         {
             LastKeepAliveDate = DateTime.UtcNow;
             return SendAsync(
-                new WebSocketMessage<string>
+                new OutboundWebSocketMessage
                 {
                     MessageId = Guid.NewGuid(),
                     MessageType = SessionMessageType.KeepAlive

+ 0 - 13
Emby.Server.Implementations/IO/ExtendedFileSystemInfo.cs

@@ -1,13 +0,0 @@
-#pragma warning disable CS1591
-
-namespace Emby.Server.Implementations.IO
-{
-    public class ExtendedFileSystemInfo
-    {
-        public bool IsHidden { get; set; }
-
-        public bool IsReadOnly { get; set; }
-
-        public bool Exists { get; set; }
-    }
-}

+ 11 - 35
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -267,25 +267,6 @@ namespace Emby.Server.Implementations.IO
             return result;
         }
 
-        private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path)
-        {
-            var result = new ExtendedFileSystemInfo();
-
-            var info = new FileInfo(path);
-
-            if (info.Exists)
-            {
-                result.Exists = true;
-
-                var attributes = info.Attributes;
-
-                result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
-                result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
-            }
-
-            return result;
-        }
-
         /// <summary>
         /// Takes a filename and removes invalid characters.
         /// </summary>
@@ -405,19 +386,18 @@ namespace Emby.Server.Implementations.IO
                 return;
             }
 
-            var info = GetExtendedFileSystemInfo(path);
+            var info = new FileInfo(path);
 
-            if (info.Exists && info.IsHidden != isHidden)
+            if (info.Exists &&
+                ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
             {
                 if (isHidden)
                 {
-                    File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
+                    File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
                 }
                 else
                 {
-                    var attributes = File.GetAttributes(path);
-                    attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
-                    File.SetAttributes(path, attributes);
+                    File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
                 }
             }
         }
@@ -430,19 +410,20 @@ namespace Emby.Server.Implementations.IO
                 return;
             }
 
-            var info = GetExtendedFileSystemInfo(path);
+            var info = new FileInfo(path);
 
             if (!info.Exists)
             {
                 return;
             }
 
-            if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
+            if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
+                && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
             {
                 return;
             }
 
-            var attributes = File.GetAttributes(path);
+            var attributes = info.Attributes;
 
             if (readOnly)
             {
@@ -450,7 +431,7 @@ namespace Emby.Server.Implementations.IO
             }
             else
             {
-                attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
+                attributes &= ~FileAttributes.ReadOnly;
             }
 
             if (isHidden)
@@ -459,17 +440,12 @@ namespace Emby.Server.Implementations.IO
             }
             else
             {
-                attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
+                attributes &= ~FileAttributes.Hidden;
             }
 
             File.SetAttributes(path, attributes);
         }
 
-        private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
-        {
-            return attributes & ~attributesToRemove;
-        }
-
         /// <summary>
         /// Swaps the files.
         /// </summary>

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System.Collections.Generic;

+ 0 - 2
Emby.Server.Implementations/Images/FolderImageProvider.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using MediaBrowser.Common.Configuration;

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System.Collections.Generic;

+ 36 - 26
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -113,6 +113,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="memoryCache">The memory cache.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="directoryService">The directory service.</param>
         public LibraryManager(
             IServerApplicationHost appHost,
             ILoggerFactory loggerFactory,
@@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
             IItemRepository itemRepository,
             IImageProcessor imageProcessor,
             IMemoryCache memoryCache,
-            NamingOptions namingOptions)
+            NamingOptions namingOptions,
+            IDirectoryService directoryService)
         {
             _appHost = appHost;
             _logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
             _memoryCache = memoryCache;
             _namingOptions = namingOptions;
 
-            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
+            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
@@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library
             }
 
             var children = item.IsFolder
-                ? ((Folder)item).GetRecursiveChildren(false).ToList()
-                : new List<BaseItem>();
+                ? ((Folder)item).GetRecursiveChildren(false)
+                : Enumerable.Empty<BaseItem>();
 
             foreach (var metadataPath in GetMetadataPaths(item, children))
             {
@@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
                 collectionType = GetContentTypeOverride(fullPath, true);
             }
 
-            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
+            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
             {
                 Parent = parent,
                 FileInfo = fileInfo,
@@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+                    SetTopParentIdsOrAncestors(query, new[] { parent });
                 }
             }
 
@@ -1262,7 +1264,14 @@ namespace Emby.Server.Implementations.Library
                 AddUserToQuery(query, query.User, allowExternalContent);
             }
 
-            return _itemRepository.GetItemList(query);
+            var itemList = _itemRepository.GetItemList(query);
+            var user = query.User;
+            if (user is not null)
+            {
+                return itemList.Where(i => i.IsVisible(user)).ToList();
+            }
+
+            return itemList;
         }
 
         public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1277,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+                    SetTopParentIdsOrAncestors(query, new[] { parent });
                 }
             }
 
@@ -1435,7 +1444,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 {
-                    SetTopParentIdsOrAncestors(query, new List<BaseItem> { parent });
+                    SetTopParentIdsOrAncestors(query, new[] { parent });
                 }
             }
 
@@ -1455,7 +1464,7 @@ namespace Emby.Server.Implementations.Library
                 _itemRepository.GetItemList(query));
         }
 
-        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents)
+        private void SetTopParentIdsOrAncestors(InternalItemsQuery query, IReadOnlyCollection<BaseItem> parents)
         {
             if (parents.All(i => i is ICollectionFolder || i is UserView))
             {
@@ -1501,6 +1510,12 @@ namespace Emby.Server.Implementations.Library
                 });
 
                 query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).ToArray();
+
+                // Prevent searching in all libraries due to empty filter
+                if (query.TopParentIds.Length == 0)
+                {
+                    query.TopParentIds = new[] { Guid.NewGuid() };
+                }
             }
         }
 
@@ -1602,7 +1617,7 @@ namespace Emby.Server.Implementations.Library
             {
                 _logger.LogError(ex, "Error getting intros");
 
-                return new List<IntroInfo>();
+                return Enumerable.Empty<IntroInfo>();
             }
         }
 
@@ -1877,7 +1892,7 @@ namespace Emby.Server.Implementations.Library
                 catch (Exception ex)
                 {
                     _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
-                    size = new ImageDimensions(0, 0);
+                    size = default;
                     image.Width = 0;
                     image.Height = 0;
                 }
@@ -2741,9 +2756,7 @@ namespace Emby.Server.Implementations.Library
                 }
             })
             .Where(i => i is not null)
-            .Where(i => query.User is null ?
-                true :
-                i.IsVisible(query.User))
+            .Where(i => query.User is null || i.IsVisible(query.User))
             .ToList();
         }
 
@@ -2876,7 +2889,7 @@ namespace Emby.Server.Implementations.Library
 
         private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
         {
-            var personsToSave = new List<BaseItem>();
+            List<BaseItem> personsToSave = null;
 
             foreach (var person in people)
             {
@@ -2918,12 +2931,12 @@ namespace Emby.Server.Implementations.Library
 
                 if (saveEntity)
                 {
-                    personsToSave.Add(personEntity);
+                    (personsToSave ??= new()).Add(personEntity);
                     await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
                 }
             }
 
-            if (personsToSave.Count > 0)
+            if (personsToSave is not null)
             {
                 CreateItems(personsToSave, null, CancellationToken.None);
             }
@@ -3085,22 +3098,19 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(path));
             }
 
-            var removeList = new List<NameValuePair>();
+            List<NameValuePair> removeList = null;
 
             foreach (var contentType in _configurationManager.Configuration.ContentTypes)
             {
-                if (string.IsNullOrWhiteSpace(contentType.Name))
-                {
-                    removeList.Add(contentType);
-                }
-                else if (_fileSystem.AreEqual(path, contentType.Name)
+                if (string.IsNullOrWhiteSpace(contentType.Name)
+                    || _fileSystem.AreEqual(path, contentType.Name)
                     || _fileSystem.ContainsSubPath(path, contentType.Name))
                 {
-                    removeList.Add(contentType);
+                    (removeList ??= new()).Add(contentType);
                 }
             }
 
-            if (removeList.Count > 0)
+            if (removeList is not null)
             {
                 _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
                     .Except(removeList)

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

@@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Library
             // If file is strm or main media stream is missing, force a metadata refresh with remote probing
             if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
                 && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
-                    || (item.MediaType == MediaType.Video && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Video))
-                    || (item.MediaType == MediaType.Audio && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio))))
+                    || (item.MediaType == MediaType.Video && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Video))
+                    || (item.MediaType == MediaType.Audio && mediaSources[0].MediaStreams.All(i => i.Type != MediaStreamType.Audio))))
             {
                 await item.RefreshMetadata(
                     new MetadataRefreshOptions(_directoryService)

+ 80 - 18
Emby.Server.Implementations/Library/PathExtensions.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using MediaBrowser.Common.Providers;
 
 namespace Emby.Server.Implementations.Library
@@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
                 return false;
             }
 
-            char oldDirectorySeparatorChar;
-            char newDirectorySeparatorChar;
-            // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
-            // The reasoning behind this is that a forward slash likely means it's a Linux path and
-            // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
-            if (newSubPath.Contains('/', StringComparison.Ordinal))
-            {
-                oldDirectorySeparatorChar = '\\';
-                newDirectorySeparatorChar = '/';
-            }
-            else
-            {
-                oldDirectorySeparatorChar = '/';
-                newDirectorySeparatorChar = '\\';
-            }
-
-            path = path.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
-            subPath = subPath.Replace(oldDirectorySeparatorChar, newDirectorySeparatorChar);
+            subPath = subPath.NormalizePath(out var newDirectorySeparatorChar);
+            path = path.NormalizePath(newDirectorySeparatorChar);
 
             // We have to ensure that the sub path ends with a directory separator otherwise we'll get weird results
             // when the sub path matches a similar but in-complete subpath
@@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
 
             return true;
         }
+
+        /// <summary>
+        /// Retrieves the full resolved path and normalizes path separators to the <see cref="Path.DirectorySeparatorChar"/>.
+        /// </summary>
+        /// <param name="path">The path to canonicalize.</param>
+        /// <returns>The fully expanded, normalized path.</returns>
+        public static string Canonicalize(this string path)
+        {
+            return Path.GetFullPath(path).NormalizePath();
+        }
+
+        /// <summary>
+        /// Normalizes the path's directory separator character to the currently defined <see cref="Path.DirectorySeparatorChar"/>.
+        /// </summary>
+        /// <param name="path">The path to normalize.</param>
+        /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
+        [return: NotNullIfNotNull(nameof(path))]
+        public static string? NormalizePath(this string? path)
+        {
+            return path.NormalizePath(Path.DirectorySeparatorChar);
+        }
+
+        /// <summary>
+        /// Normalizes the path's directory separator character.
+        /// </summary>
+        /// <param name="path">The path to normalize.</param>
+        /// <param name="separator">The separator character the path now uses or <see langword="null"/>.</param>
+        /// <returns>The normalized path string or <see langword="null"/> if the input path is null or empty.</returns>
+        [return: NotNullIfNotNull(nameof(path))]
+        public static string? NormalizePath(this string? path, out char separator)
+        {
+            if (string.IsNullOrEmpty(path))
+            {
+                separator = default;
+                return path;
+            }
+
+            var newSeparator = '\\';
+
+            // True normalization is still not possible https://github.com/dotnet/runtime/issues/2162
+            // The reasoning behind this is that a forward slash likely means it's a Linux path and
+            // so the whole path should be normalized to use / and vice versa for Windows (although Windows doesn't care much).
+            if (path.Contains('/', StringComparison.Ordinal))
+            {
+                newSeparator = '/';
+            }
+
+            separator = newSeparator;
+
+            return path.NormalizePath(newSeparator);
+        }
+
+        /// <summary>
+        /// Normalizes the path's directory separator character to the specified character.
+        /// </summary>
+        /// <param name="path">The path to normalize.</param>
+        /// <param name="newSeparator">The replacement directory separator character. Must be a valid directory separator.</param>
+        /// <returns>The normalized path.</returns>
+        /// <exception cref="ArgumentException">Thrown if the new separator character is not a directory separator.</exception>
+        [return: NotNullIfNotNull(nameof(path))]
+        public static string? NormalizePath(this string? path, char newSeparator)
+        {
+            const char Bs = '\\';
+            const char Fs = '/';
+
+            if (!(newSeparator == Bs || newSeparator == Fs))
+            {
+                throw new ArgumentException("The character must be a directory separator.");
+            }
+
+            if (string.IsNullOrEmpty(path))
+            {
+                return path;
+            }
+
+            return newSeparator == Bs ? path.Replace(Fs, newSeparator) : path.Replace(Bs, newSeparator);
+        }
     }
 }

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

@@ -158,7 +158,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
         {
             var files = new List<FileSystemMetadata>();
-            var items = new List<BaseItem>();
             var leftOver = new List<FileSystemMetadata>();
 
             // Loop through each child file/folder and see if we find a video
@@ -180,7 +179,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             var result = new MultiItemResolverResult
             {
                 ExtraFiles = leftOver,
-                Items = items
+                Items = new List<BaseItem>()
             };
 
             var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
@@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                     continue;
                 }
 
-                if (resolvedItem.Files.Count == 0)
+                // Until multi-part books are handled letting files stack hides them from browsing in the client
+                if (resolvedItem.Files.Count == 0 || resolvedItem.Extras.Count > 0 || resolvedItem.AlternateVersions.Count > 0)
                 {
                     continue;
                 }

+ 5 - 2
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -25,16 +25,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     {
         private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</param>
-        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
+        /// <param name="directoryService">The directory service.</param>
+        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
         {
             _logger = logger;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
 
         /// <summary>
@@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 }
 
                 // If args contains music it's a music album
-                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
+                if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
                 {
                     return true;
                 }

+ 9 - 6
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
 using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
@@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
         private readonly ILogger<MusicAlbumResolver> _logger;
-        private NamingOptions _namingOptions;
+        private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
         /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
         /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
+        /// <param name="directoryService">The directory service.</param>
         public MusicArtistResolver(
             ILogger<MusicAlbumResolver> logger,
-            NamingOptions namingOptions)
+            NamingOptions namingOptions,
+            IDirectoryService directoryService)
         {
             _logger = logger;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
 
         /// <summary>
@@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return null;
             }
 
-            var directoryService = args.DirectoryService;
-
-            var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
+            var albumResolver = new MusicAlbumResolver(_logger, _namingOptions, _directoryService);
 
             var directories = args.FileSystemChildren.Where(i => i.IsDirectory);
 
@@ -97,7 +100,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 }
 
                 // If we contain a music album assume we are an artist folder
-                if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, directoryService))
+                if (albumResolver.IsMusicAlbum(fileSystemInfo.FullName, _directoryService))
                 {
                     // Stop once we see a music album
                     state.Stop();

+ 21 - 5
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -25,14 +25,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
     {
         private readonly ILogger _logger;
 
-        protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
+        protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
         {
             _logger = logger;
             NamingOptions = namingOptions;
+            DirectoryService = directoryService;
         }
 
         protected NamingOptions NamingOptions { get; }
 
+        protected IDirectoryService DirectoryService { get; }
+
         /// <summary>
         /// Resolves the specified args.
         /// </summary>
@@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     var filename = child.Name;
                     if (child.IsDirectory)
                     {
-                        if (IsDvdDirectory(child.FullName, filename, args.DirectoryService))
+                        if (IsDvdDirectory(child.FullName, filename, DirectoryService))
                         {
-                            videoType = VideoType.Dvd;
+                            var videoTmp = new TVideoType
+                            {
+                                Path = args.Path,
+                                VideoType = VideoType.Dvd
+                            };
+                            Set3DFormat(videoTmp);
+                            return videoTmp;
                         }
-                        else if (IsBluRayDirectory(filename))
+
+                        if (IsBluRayDirectory(filename))
                         {
-                            videoType = VideoType.BluRay;
+                            var videoTmp = new TVideoType
+                            {
+                                Path = args.Path,
+                                VideoType = VideoType.BluRay
+                            };
+                            Set3DFormat(videoTmp);
+                            return videoTmp;
                         }
                     }
                     else if (IsDvdFile(filename))

+ 13 - 4
Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs

@@ -4,6 +4,8 @@ using System.IO;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
@@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// <summary>
     /// Resolves a Path into a Video or Video subclass.
     /// </summary>
-    internal class ExtraResolver
+    internal class ExtraResolver : BaseVideoResolver<Video>
     {
         private readonly NamingOptions _namingOptions;
         private readonly IItemResolver[] _trailerResolvers;
@@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
-        public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions)
+        /// <param name="directoryService">The directory service.</param>
+        public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+            : base(logger, namingOptions, directoryService)
         {
             _namingOptions = namingOptions;
-            _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) };
-            _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) };
+            _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
+            _videoResolvers = new IItemResolver[] { this };
+        }
+
+        protected override Video Resolve(ItemResolveArgs args)
+        {
+            return ResolveVideo<Video>(args, true);
         }
 
         /// <summary>

+ 4 - 2
Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs

@@ -2,6 +2,7 @@
 
 using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.Library.Resolvers
@@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</param>
-        public GenericVideoResolver(ILogger logger, NamingOptions namingOptions)
-            : base(logger, namingOptions)
+        /// <param name="directoryService">The directory service.</param>
+        public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
+            : base(logger, namingOptions, directoryService)
         {
         }
     }

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

@@ -43,8 +43,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</param>
-        public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions)
-            : base(logger, namingOptions)
+        /// <param name="directoryService">The directory service.</param>
+        public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+            : base(logger, namingOptions, directoryService)
         {
             _imageProcessor = imageProcessor;
         }
@@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
                 if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                    movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
                 }
 
                 if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
                 {
-                    movie = FindMovie<Video>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                    movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
                 }
 
                 if (string.IsNullOrEmpty(collectionType))
@@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                         return null;
                     }
 
-                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
                 }
 
                 if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
                 {
-                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, true);
+                    movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
                 }
 
                 // ignore extras
@@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return result;
         }
 
-        private static bool IsIgnored(string filename)
-        {
-            // Ignore samples
-            Match m = Regex.Match(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
-            return m.Success;
-        }
+        private static bool IsIgnored(ReadOnlySpan<char> filename)
+            => Regex.IsMatch(filename, @"\bsample\b", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 
         private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
         {

+ 14 - 4
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -1,7 +1,5 @@
 #nullable disable
 
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.IO;
@@ -12,15 +10,20 @@ using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
+    /// <summary>
+    /// Class PhotoResolver.
+    /// </summary>
     public class PhotoResolver : ItemResolver<Photo>
     {
         private readonly IImageProcessor _imageProcessor;
         private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
         private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
@@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
             "default"
         };
 
-        public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
+        /// <summary>
+        /// Initializes a new instance of the <see cref="PhotoResolver"/> class.
+        /// </summary>
+        /// <param name="imageProcessor">The image processor.</param>
+        /// <param name="namingOptions">The naming options.</param>
+        /// <param name="directoryService">The directory service.</param>
+        public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService)
         {
             _imageProcessor = imageProcessor;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
 
         /// <summary>
@@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                         var filename = Path.GetFileNameWithoutExtension(args.Path);
 
                         // Make sure the image doesn't belong to a video file
-                        var files = args.DirectoryService.GetFiles(Path.GetDirectoryName(args.Path));
+                        var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
 
                         foreach (var file in files)
                         {

+ 7 - 4
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         {
             if (args.IsDirectory)
             {
-                // It's a boxset if the path is a directory with [playlist] in it's the name
+                // It's a boxset if the path is a directory with [playlist] in its name
                 var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
                 if (string.IsNullOrEmpty(filename))
                 {
@@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim()
+                        Name = filename.Replace("[playlist]", string.Empty, StringComparison.OrdinalIgnoreCase).Trim(),
+                        OpenAccess = true
                     };
                 }
 
@@ -53,7 +54,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     {
                         Path = args.Path,
-                        Name = filename
+                        Name = filename,
+                        OpenAccess = true
                     };
                 }
             }
@@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                         Path = args.Path,
                         Name = Path.GetFileNameWithoutExtension(args.Path),
                         IsInMixedFolder = true,
-                        PlaylistMediaType = MediaType.Audio
+                        PlaylistMediaType = MediaType.Audio,
+                        OpenAccess = true
                     };
                 }
             }

+ 4 - 2
Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs

@@ -5,6 +5,7 @@ using System.Linq;
 using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 
@@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</param>
-        public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions)
-            : base(logger, namingOptions)
+        /// <param name="directoryService">The directory service.</param>
+        public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions, IDirectoryService directoryService)
+            : base(logger, namingOptions, directoryService)
         {
         }
 

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

@@ -81,14 +81,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 if (season.IndexNumber.HasValue)
                 {
                     var seasonNumber = season.IndexNumber.Value;
-
-                    season.Name = seasonNumber == 0 ?
-                        args.LibraryOptions.SeasonZeroDisplayName :
-                        string.Format(
-                            CultureInfo.InvariantCulture,
-                            _localization.GetLocalizedString("NameSeasonNumber"),
-                            seasonNumber,
-                            args.LibraryOptions.PreferredMetadataLanguage);
+                    if (string.IsNullOrEmpty(season.Name))
+                    {
+                        var seasonNames = series.SeasonNames;
+                        if (seasonNames.TryGetValue(seasonNumber, out var seasonName))
+                        {
+                            season.Name = seasonName;
+                        }
+                        else
+                        {
+                            season.Name = seasonNumber == 0 ?
+                                args.LibraryOptions.SeasonZeroDisplayName :
+                                string.Format(
+                                    CultureInfo.InvariantCulture,
+                                    _localization.GetLocalizedString("NameSeasonNumber"),
+                                    seasonNumber,
+                                    args.LibraryOptions.PreferredMetadataLanguage);
+                        }
+                    }
                 }
 
                 return season;

+ 6 - 0
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -184,6 +184,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         {
             var justName = Path.GetFileName(path.AsSpan());
 
+            var imdbId = justName.GetAttributeValue("imdbid");
+            if (!string.IsNullOrEmpty(imdbId))
+            {
+                item.SetProviderId(MetadataProvider.Imdb, imdbId);
+            }
+
             var tvdbId = justName.GetAttributeValue("tvdbid");
             if (!string.IsNullOrEmpty(tvdbId))
             {

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

@@ -46,10 +46,9 @@ namespace Emby.Server.Implementations.Library
         public Folder[] GetUserViews(UserViewQuery query)
         {
             var user = _userManager.GetUserById(query.UserId);
-
             if (user is null)
             {
-                throw new ArgumentException("User Id specified in the query does not exist.", nameof(query));
+                throw new ArgumentException("User id specified in the query does not exist.", nameof(query));
             }
 
             var folders = _libraryManager.GetUserRootFolder()
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
                 .ToList();
 
             var groupedFolders = new List<ICollectionFolder>();
-
             var list = new List<Folder>();
 
             foreach (var folder in folders)
@@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
                 var collectionFolder = folder as ICollectionFolder;
                 var folderViewType = collectionFolder?.CollectionType;
 
+                // Playlist library requires special handling because the folder only refrences user playlists
+                if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+                {
+                    var items = folder.GetItemList(new InternalItemsQuery(user)
+                    {
+                        ParentId = folder.ParentId
+                    });
+
+                    if (!items.Any(item => item.IsVisible(user)))
+                    {
+                        continue;
+                    }
+                }
+
                 if (UserView.IsUserSpecific(folder))
                 {
                     list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
@@ -111,10 +123,10 @@ namespace Emby.Server.Implementations.Library
 
             if (query.IncludeExternalContent)
             {
-                var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
+                var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
                 {
                     UserId = query.UserId
-                });
+                }).GetAwaiter().GetResult();
 
                 var channels = channelResult.Items;
 
@@ -132,14 +144,12 @@ namespace Emby.Server.Implementations.Library
             }
 
             var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
-
             var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
 
             return list
                 .OrderBy(i =>
                 {
                     var index = Array.IndexOf(orders, i.Id);
-
                     if (index == -1
                         && i is UserView view
                         && !view.DisplayParentId.Equals(default))
@@ -286,7 +296,7 @@ namespace Emby.Server.Implementations.Library
 
             if (parents.Count == 0)
             {
-                return new List<BaseItem>();
+                return Array.Empty<BaseItem>();
             }
 
             if (includeItemTypes.Length == 0)

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

@@ -627,10 +627,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     _timerProvider.Update(existingTimer);
                     return Task.FromResult(existingTimer.Id);
                 }
-                else
-                {
-                    throw new ArgumentException("A scheduled recording already exists for this program.");
-                }
+
+                throw new ArgumentException("A scheduled recording already exists for this program.");
             }
 
             info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
@@ -1866,8 +1864,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 {
                     await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
                     await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
-                    string id;
-                    if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id))
+                    if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out var id))
                     {
                         await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false);
                     }
@@ -2032,7 +2029,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
 
                     var directors = people
-                        .Where(i => IsPersonType(i, PersonType.Director))
+                        .Where(i => i.IsType(PersonKind.Director))
                         .Select(i => i.Name)
                         .ToList();
 
@@ -2042,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
 
                     var writers = people
-                        .Where(i => IsPersonType(i, PersonType.Writer))
+                        .Where(i => i.IsType(PersonKind.Writer))
                         .Select(i => i.Name)
                         .Distinct(StringComparer.OrdinalIgnoreCase)
                         .ToList();
@@ -2122,10 +2119,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             }
         }
 
-        private static bool IsPersonType(PersonInfo person, string type)
-            => string.Equals(person.Type, type, StringComparison.OrdinalIgnoreCase)
-                || string.Equals(person.Role, type, StringComparison.OrdinalIgnoreCase);
-
         private LiveTvProgram GetProgramInfoFromCache(string programId)
         {
             var query = new InternalItemsQuery

+ 11 - 14
Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs

@@ -415,14 +415,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
                 return null;
             }
-            else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
+
+            if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
             {
                 return uri;
             }
-            else
-            {
-                return apiUrl + "/image/" + uri + "?token=" + token;
-            }
+
+            return apiUrl + "/image/" + uri + "?token=" + token;
         }
 
         private static double GetAspectRatio(ImageDataDto i)
@@ -463,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
 
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
-            foreach (ReadOnlySpan<char> i in programIds)
+            foreach (var i in programIds)
             {
                 str.Append('"')
-                    .Append(i.Slice(0, 10))
+                    .Append(i[..10])
                     .Append("\",");
             }
 
@@ -570,15 +569,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 _tokens.TryAdd(username, savedToken);
             }
 
-            if (!string.IsNullOrEmpty(savedToken.Name) && !string.IsNullOrEmpty(savedToken.Value))
+            if (!string.IsNullOrEmpty(savedToken.Name)
+                && long.TryParse(savedToken.Value, CultureInfo.InvariantCulture, out long ticks))
             {
-                if (long.TryParse(savedToken.Value, NumberStyles.Any, CultureInfo.InvariantCulture, out long ticks))
+                // If it's under 24 hours old we can still use it
+                if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
                 {
-                    // If it's under 24 hours old we can still use it
-                    if (DateTime.UtcNow.Ticks - ticks < TimeSpan.FromHours(20).Ticks)
-                    {
-                        return savedToken.Name;
-                    }
+                    return savedToken.Name;
                 }
             }
 

+ 12 - 11
Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs

@@ -137,32 +137,33 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         private static ProgramInfo GetProgramInfo(XmlTvProgram program, ListingsProviderInfo info)
         {
-            string episodeTitle = program.Episode?.Title;
+            string episodeTitle = program.Episode.Title;
+            var programCategories = program.Categories.Where(c => !string.IsNullOrWhiteSpace(c)).ToList();
 
             var programInfo = new ProgramInfo
             {
                 ChannelId = program.ChannelId,
                 EndDate = program.EndDate.UtcDateTime,
-                EpisodeNumber = program.Episode?.Episode,
+                EpisodeNumber = program.Episode.Episode,
                 EpisodeTitle = episodeTitle,
-                Genres = program.Categories,
+                Genres = programCategories,
                 StartDate = program.StartDate.UtcDateTime,
                 Name = program.Title,
                 Overview = program.Description,
                 ProductionYear = program.CopyrightDate?.Year,
-                SeasonNumber = program.Episode?.Series,
-                IsSeries = program.Episode is not null,
+                SeasonNumber = program.Episode.Series,
+                IsSeries = program.Episode.Series is not null,
                 IsRepeat = program.IsPreviouslyShown && !program.IsNew,
                 IsPremiere = program.Premiere is not null,
-                IsKids = program.Categories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsMovie = program.Categories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsNews = program.Categories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
-                IsSports = program.Categories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsKids = programCategories.Any(c => info.KidsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsMovie = programCategories.Any(c => info.MovieCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsNews = programCategories.Any(c => info.NewsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
+                IsSports = programCategories.Any(c => info.SportsCategories.Contains(c, StringComparison.OrdinalIgnoreCase)),
                 ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
                 HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
                 OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
                 CommunityRating = program.StarRating,
-                SeriesId = program.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
+                SeriesId = program.Episode.Episode is null ? null : program.Title?.GetMD5().ToString("N", CultureInfo.InvariantCulture)
             };
 
             if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
                 Id = c.Id,
                 Name = c.DisplayName,
-                ImageUrl = string.IsNullOrEmpty(c.Icon.Source) ? null : c.Icon.Source,
+                ImageUrl = string.IsNullOrEmpty(c.Icon?.Source) ? null : c.Icon.Source,
                 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
             }).ToList();
         }

+ 16 - 16
Emby.Server.Implementations/LiveTv/LiveTvManager.cs

@@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
             return 7;
         }
 
-        private QueryResult<BaseItem> GetEmbyRecordings(RecordingQuery query, DtoOptions dtoOptions, User user)
+        private async Task<QueryResult<BaseItem>> GetEmbyRecordingsAsync(RecordingQuery query, DtoOptions dtoOptions, User user)
         {
             if (user is null)
             {
                 return new QueryResult<BaseItem>();
             }
 
-            var folderIds = GetRecordingFolders(user, true)
-                .Select(i => i.Id)
-                .ToList();
+            var folders = await GetRecordingFoldersAsync(user, true).ConfigureAwait(false);
+            var folderIds = Array.ConvertAll(folders, x => x.Id);
 
             var excludeItemTypes = new List<BaseItemKind>();
 
-            if (folderIds.Count == 0)
+            if (folderIds.Length == 0)
             {
                 return new QueryResult<BaseItem>();
             }
@@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 MediaTypes = new[] { MediaType.Video },
                 Recursive = true,
-                AncestorIds = folderIds.ToArray(),
+                AncestorIds = folderIds,
                 IsFolder = false,
                 IsVirtualItem = false,
                 Limit = limit,
@@ -1528,7 +1527,7 @@ namespace Emby.Server.Implementations.LiveTv
             }
         }
 
-        public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options)
+        public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options)
         {
             var user = query.UserId.Equals(default)
                 ? null
@@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
 
             RemoveFields(options);
 
-            var internalResult = GetEmbyRecordings(query, options, user);
+            var internalResult = await GetEmbyRecordingsAsync(query, options, user).ConfigureAwait(false);
 
             var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
 
@@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
             return _tvDtoService.GetInternalProgramId(externalId);
         }
 
-        public List<BaseItem> GetRecordingFolders(User user)
-        {
-            return GetRecordingFolders(user, false);
-        }
+        /// <inheritdoc />
+        public Task<BaseItem[]> GetRecordingFoldersAsync(User user)
+            => GetRecordingFoldersAsync(user, false);
 
-        private List<BaseItem> GetRecordingFolders(User user, bool refreshChannels)
+        private async Task<BaseItem[]> GetRecordingFoldersAsync(User user, bool refreshChannels)
         {
             var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
                 .SelectMany(i => i.Locations)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
                 .OrderBy(i => i.SortName)
                 .ToList();
 
-            folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+            var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
             {
                 UserId = user.Id,
                 IsRecordingsFolder = true,
                 RefreshLatestChannelItems = refreshChannels
-            }).Items);
+            }).ConfigureAwait(false);
+
+            folders.AddRange(channels.Items);
 
-            return folders.Cast<BaseItem>().ToList();
+            return folders.Cast<BaseItem>().ToArray();
         }
     }
 }

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

@@ -14,6 +14,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
@@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _socketFactory = socketFactory;
             _streamHelper = streamHelper;
 
-            _jsonOptions = JsonDefaults.Options;
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+            _jsonOptions.Converters.Add(new JsonBoolNumberConverter());
         }
 
         public string Name => "HD Homerun";

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

@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
         {
             using var client = new TcpClient();
-            await client.ConnectAsync(remoteIp, HdHomeRunPort).ConfigureAwait(false);
+            await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
 
             using var stream = client.GetStream();
             return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);

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

@@ -13,8 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public LegacyHdHomerunChannelCommands(string url)
         {
             // parse url for channel and program
-            var regExp = new Regex(@"\/ch([0-9]+)-?([0-9]*)");
-            var match = regExp.Match(url);
+            var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
             if (match.Success)
             {
                 _channel = match.Groups[1].Value;

+ 14 - 7
Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs

@@ -30,12 +30,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost
     {
-        private static readonly string[] _disallowedSharedStreamExtensions =
+        private static readonly string[] _disallowedMimeTypes =
         {
-            ".mkv",
-            ".mp4",
-            ".m3u8",
-            ".mpd"
+            "video/x-matroska",
+            "video/mp4",
+            "application/vnd.apple.mpegurl",
+            "application/mpegurl",
+            "application/x-mpegurl",
+            "video/vnd.mpeg.dash.mpd"
         };
 
         private readonly IHttpClientFactory _httpClientFactory;
@@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
             {
-                var extension = Path.GetExtension(mediaSource.Path) ?? string.Empty;
+                using var message = new HttpRequestMessage(HttpMethod.Head, mediaSource.Path);
+                using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
+                    .SendAsync(message, cancellationToken)
+                    .ConfigureAwait(false);
 
-                if (!_disallowedSharedStreamExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
+                response.EnsureSuccessStatusCode();
+
+                if (!_disallowedMimeTypes.Contains(response.Content.Headers.ContentType?.ToString(), StringComparison.OrdinalIgnoreCase))
                 {
                     return new SharedHttpStream(mediaSource, tunerHost, streamId, FileSystem, _httpClientFactory, Logger, Config, _appHost, _streamHelper);
                 }

+ 20 - 27
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -122,9 +122,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var attributes = ParseExtInf(extInf, out string remaining);
             extInf = remaining;
 
-            if (attributes.TryGetValue("tvg-logo", out string value))
+            if (attributes.TryGetValue("tvg-logo", out string tvgLogo))
             {
-                channel.ImageUrl = value;
+                channel.ImageUrl = tvgLogo;
+            }
+            else if (attributes.TryGetValue("logo", out string logo))
+            {
+                channel.ImageUrl = logo;
             }
 
             if (attributes.TryGetValue("group-title", out string groupTitle))
@@ -166,30 +170,25 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 
             string numberString = null;
-            string attributeValue;
 
-            if (attributes.TryGetValue("tvg-chno", out attributeValue))
+            if (attributes.TryGetValue("tvg-chno", out var attributeValue)
+                && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
             {
-                if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
-                {
-                    numberString = attributeValue;
-                }
+                numberString = attributeValue;
             }
 
             if (!IsValidChannelNumber(numberString))
             {
                 if (attributes.TryGetValue("tvg-id", out attributeValue))
                 {
-                    if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+                    if (double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
                     {
                         numberString = attributeValue;
                     }
-                    else if (attributes.TryGetValue("channel-id", out attributeValue))
+                    else if (attributes.TryGetValue("channel-id", out attributeValue)
+                        && double.TryParse(attributeValue, CultureInfo.InvariantCulture, out _))
                     {
-                        if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
-                        {
-                            numberString = attributeValue;
-                        }
+                        numberString = attributeValue;
                     }
                 }
 
@@ -207,7 +206,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                         {
                             var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' });
 
-                            if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+                            if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
                             {
                                 numberString = numberPart.ToString();
                             }
@@ -255,19 +254,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
         private static bool IsValidChannelNumber(string numberString)
         {
-            if (string.IsNullOrWhiteSpace(numberString) ||
-                string.Equals(numberString, "-1", StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(numberString, "0", StringComparison.OrdinalIgnoreCase))
-            {
-                return false;
-            }
-
-            if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+            if (string.IsNullOrWhiteSpace(numberString)
+                || string.Equals(numberString, "-1", StringComparison.Ordinal)
+                || string.Equals(numberString, "0", StringComparison.Ordinal))
             {
                 return false;
             }
 
-            return true;
+            return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
         }
 
         private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
@@ -285,7 +279,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 {
                     var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });
 
-                    if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _))
+                    if (double.TryParse(numberPart, CultureInfo.InvariantCulture, out _))
                     {
                         // channel.Number = number.ToString();
                         nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
@@ -317,8 +311,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            var reg = new Regex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
-            var matches = reg.Matches(line);
+            var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
 
             remaining = line;
 

+ 11 - 36
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             OriginalStreamId = originalStreamId;
-            EnableStreamSharing = true;
         }
 
         public override async Task Open(CancellationToken openCancellationToken)
@@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
                 .ConfigureAwait(false);
 
-            var contentType = response.Content.Headers.ContentType?.ToString() ?? string.Empty;
-            if (contentType.Contains("matroska", StringComparison.OrdinalIgnoreCase)
-                || contentType.Contains("mp4", StringComparison.OrdinalIgnoreCase)
-                || contentType.Contains("dash", StringComparison.OrdinalIgnoreCase)
-                || contentType.Contains("mpegURL", StringComparison.OrdinalIgnoreCase)
-                || contentType.Contains("text/", StringComparison.OrdinalIgnoreCase))
-            {
-                // Close the stream without any sharing features
-                response.Dispose();
-                return;
-            }
-
-            SetTempFilePath("ts");
-
             var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 
             _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
 
-            // OpenedMediaSource.Protocol = MediaProtocol.File;
-            // OpenedMediaSource.Path = tempFile;
-            // OpenedMediaSource.ReadAtNativeFramerate = true;
-
             MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
 
-            // OpenedMediaSource.Path = TempFilePath;
-            // OpenedMediaSource.Protocol = MediaProtocol.File;
-
-            // OpenedMediaSource.Path = _tempFilePath;
-            // OpenedMediaSource.Protocol = MediaProtocol.File;
-            // OpenedMediaSource.SupportsDirectPlay = false;
-            // OpenedMediaSource.SupportsDirectStream = true;
-            // OpenedMediaSource.SupportsTranscoding = true;
             var res = await taskCompletionSource.Task.ConfigureAwait(false);
             if (!res)
             {
@@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     try
                     {
                         Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
-                        using var message = response;
-                        await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                        await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
-                        await StreamHelper.CopyToAsync(
-                            stream,
-                            fileStream,
-                            IODefaults.CopyToBufferSize,
-                            () => Resolve(openTaskCompletionSource),
-                            cancellationToken).ConfigureAwait(false);
+                        using (response)
+                        {
+                            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+                            await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
+                            await StreamHelper.CopyToAsync(
+                                stream,
+                                fileStream,
+                                IODefaults.CopyToBufferSize,
+                                () => Resolve(openTaskCompletionSource),
+                                cancellationToken).ConfigureAwait(false);
+                        }
                     }
                     catch (OperationCanceledException ex)
                     {

+ 125 - 2
Emby.Server.Implementations/Localization/Core/be.json

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

+ 22 - 16
Emby.Server.Implementations/Localization/Core/bn.json

@@ -1,27 +1,27 @@
 {
     "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
-    "Collections": "সংগ্রহ",
+    "Collections": "সংগ্রহশালা",
     "ChapterNameValue": "অধ্যায় {0}",
-    "Channels": "চ্যানেল",
+    "Channels": "চ্যানেলসমূহ",
     "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
-    "Books": "বই",
+    "Books": "পুস্তকসমূহ",
     "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
-    "Artists": "শিল্পীরা",
+    "Artists": "শিল্পীগণ",
     "Application": "অ্যাপ্লিকেশন",
-    "Albums": "অ্যালবামগুলো",
+    "Albums": "অ্যালবামসমূহ",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderAlbumArtists": "লবাম শিল্পীবৃন্দ",
-    "Genres": "শৈলী",
-    "Folders": "ফোল্ডারগুলো",
+    "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
+    "Genres": "শৈলীধারাসমূহ",
+    "Folders": "ফোল্ডারসমূহ",
     "Favorites": "পছন্দসমূহ",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
     "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
     "VersionNumber": "সংস্করণ {0}",
-    "ValueSpecialEpisodeName": "বিশেষ - {0}",
+    "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
     "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
     "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
@@ -36,10 +36,10 @@
     "User": "ব্যবহারকারী",
     "TvShows": "টিভি শোগুলো",
     "System": "সিস্টেম",
-    "Sync": "সিংক",
+    "Sync": "সমলয় স্থাপন",
     "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
     "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
-    "Songs": "গানগুলো",
+    "Songs": "সঙ্গীতসমূহ",
     "Shows": "টিভি পর্ব",
     "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
     "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
@@ -49,8 +49,8 @@
     "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
     "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
     "Plugin": "প্লাগিন",
-    "Playlists": "প্লেলিস্ট",
-    "Photos": "ছবিগুলো",
+    "Playlists": "প্লে লিস্ট সমূহ",
+    "Photos": "চিত্রসমূহ",
     "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
     "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
     "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
@@ -71,9 +71,9 @@
     "NameSeasonUnknown": "সিজন অজানা",
     "NameSeasonNumber": "সিজন {0}",
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
-    "MusicVideos": "গানের ভিডিও",
+    "MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ",
     "Music": "গান",
-    "Movies": "চলচ্চিত্র",
+    "Movies": "চলচ্চিত্রসমূহ",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
     "HeaderRecordingGroups": "রেকর্ডিং দল",
@@ -117,5 +117,11 @@
     "Forced": "জোরকরে",
     "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
     "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
-    "Default": "প্রাথমিক"
+    "Default": "ডিফল্ট",
+    "HearingImpaired": "দুর্বল শ্রবণক্ষমতাধরদের জন্য",
+    "TaskOptimizeDatabaseDescription": "তথ্যভাণ্ডার সুবিন্যস্ত করে ও অব্যবহৃত জায়গা ছেড়ে দেয়। লাইব্রেরী স্ক্যান অথবা যেকোনো তথ্যভাণ্ডার পরিবর্তনের পর এই প্রক্রিয়া চালালে তথ্যভাণ্ডারের তথ্য প্রদান দ্রুততর হতে পারে।",
+    "External": "বাহ্যিক",
+    "TaskOptimizeDatabase": "তথ্যভাণ্ডার সুবিন্যাস",
+    "TaskKeyframeExtractor": "কি-ফ্রেম নিষ্কাশক",
+    "TaskKeyframeExtractorDescription": "ভিডিয়ো থেকে কি-ফ্রেম নিষ্কাশনের মাধ্যমে অধিকতর সঠিক HLS প্লে লিস্ট তৈরী করে। এই প্রক্রিয়া দীর্ঘ সময় ধরে চলতে পারে।"
 }

+ 42 - 42
Emby.Server.Implementations/Localization/Core/ca.json

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "Books": "Llibres",
-    "CameraImageUploadedFrom": "S'ha pujat una nova imatge des de la camera desde {0}",
+    "CameraImageUploadedFrom": "S'ha pujat una nova imatge de càmera des de {0}",
     "Channels": "Canals",
     "ChapterNameValue": "Capítol {0}",
     "Collections": "Col·leccions",
@@ -16,65 +16,65 @@
     "Folders": "Carpetes",
     "Genres": "Gèneres",
     "HeaderAlbumArtists": "Artistes de l'àlbum",
-    "HeaderContinueWatching": "Continua Veient",
-    "HeaderFavoriteAlbums": "Àlbums Preferits",
-    "HeaderFavoriteArtists": "Artistes Predilectes",
-    "HeaderFavoriteEpisodes": "Episodis Predilectes",
-    "HeaderFavoriteShows": "Sèries Predilectes",
-    "HeaderFavoriteSongs": "Cançons Predilectes",
-    "HeaderLiveTV": "TV en Directe",
+    "HeaderContinueWatching": "Continuar veient",
+    "HeaderFavoriteAlbums": "Àlbums preferits",
+    "HeaderFavoriteArtists": "Artistes preferits",
+    "HeaderFavoriteEpisodes": "Episodis preferits",
+    "HeaderFavoriteShows": "Sèries preferides",
+    "HeaderFavoriteSongs": "Cançons preferides",
+    "HeaderLiveTV": "TV en directe",
     "HeaderNextUp": "A continuació",
-    "HeaderRecordingGroups": "Grups d'Enregistrament",
-    "HomeVideos": "Vídeos Domèstics",
+    "HeaderRecordingGroups": "Grups d'enregistrament",
+    "HomeVideos": "Vídeos domèstics",
     "Inherit": "Hereta",
-    "ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
-    "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
+    "ItemAddedWithName": "{0} ha sigut afegit a la biblioteca",
+    "ItemRemovedWithName": "{0} ha sigut eliminat de la biblioteca",
     "LabelIpAddressValue": "Adreça IP: {0}",
     "LabelRunningTimeValue": "Temps en funcionament: {0}",
-    "Latest": "Darreres",
-    "MessageApplicationUpdated": "El Servidor de Jellyfin ha estat actualitzat",
-    "MessageApplicationUpdatedTo": "El Servidor de Jellyfin ha estat actualitzat a {0}",
+    "Latest": "Darrers",
+    "MessageApplicationUpdated": "El servidor de Jellyfin ha estat actualitzat",
+    "MessageApplicationUpdatedTo": "El servidor de Jellyfin ha estat actualitzat a {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
     "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
     "MixedContent": "Contingut barrejat",
     "Movies": "Pel·lícules",
     "Music": "Música",
-    "MusicVideos": "Vídeos Musicals",
+    "MusicVideos": "Videoclips",
     "NameInstallFailed": "{0} instal·lació fallida",
     "NameSeasonNumber": "Temporada {0}",
-    "NameSeasonUnknown": "Temporada Desconeguda",
-    "NewVersionIsAvailable": "Una nova versió del Servidor Jellyfin està disponible per descarregar.",
-    "NotificationOptionApplicationUpdateAvailable": "Actualització d'aplicació disponible",
-    "NotificationOptionApplicationUpdateInstalled": "Actualització d'aplicació instal·lada",
+    "NameSeasonUnknown": "Temporada desconeguda",
+    "NewVersionIsAvailable": "Una nova versió del servidor de Jellyfin està disponible per a descarregar.",
+    "NotificationOptionApplicationUpdateAvailable": "Actualització de l'aplicació disponible",
+    "NotificationOptionApplicationUpdateInstalled": "Actualització de l'aplicació instal·lada",
     "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
     "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
     "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
     "NotificationOptionInstallationFailed": "Instal·lació fallida",
     "NotificationOptionNewLibraryContent": "Nou contingut afegit",
-    "NotificationOptionPluginError": "Un connector ha fallat",
-    "NotificationOptionPluginInstalled": "Connector instal·lat",
-    "NotificationOptionPluginUninstalled": "Connector desinstal·lat",
-    "NotificationOptionPluginUpdateInstalled": "Actualització de connector instal·lada",
+    "NotificationOptionPluginError": "Un complement ha fallat",
+    "NotificationOptionPluginInstalled": "Complement instal·lat",
+    "NotificationOptionPluginUninstalled": "Complement desinstal·lat",
+    "NotificationOptionPluginUpdateInstalled": "Actualització de complement instal·lada",
     "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
     "NotificationOptionTaskFailed": "Tasca programada fallida",
-    "NotificationOptionUserLockedOut": "Usuari tancat",
-    "NotificationOptionVideoPlayback": "Reproducció de video iniciada",
-    "NotificationOptionVideoPlaybackStopped": "Reproducció de video aturada",
+    "NotificationOptionUserLockedOut": "Usuari expulsat",
+    "NotificationOptionVideoPlayback": "Reproducció de vídeo iniciada",
+    "NotificationOptionVideoPlaybackStopped": "Reproducció de vídeo aturada",
     "Photos": "Fotos",
     "Playlists": "Llistes de reproducció",
-    "Plugin": "Connector",
+    "Plugin": "Complement",
     "PluginInstalledWithName": "{0} ha estat instal·lat",
     "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
     "PluginUpdatedWithName": "{0} ha estat actualitzat",
     "ProviderValue": "Proveïdor: {0}",
     "ScheduledTaskFailedWithName": "{0} ha fallat",
-    "ScheduledTaskStartedWithName": "{0} iniciat",
+    "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
     "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
     "Shows": "Sèries",
     "Songs": "Cançons",
-    "StartupEmbyServerIsLoading": "El Servidor de Jellyfin està carregant. Si et plau, prova de nou ben aviat.",
+    "StartupEmbyServerIsLoading": "El servidor de Jellyfin s'està carregant. Proveu-ho altre cop aviat.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "Els subtítols no s'han pogut baixar de {0} per {1}",
+    "SubtitleDownloadFailureFromForItem": "Els subtítols per a {1} no s'han pogut baixar de {0}",
     "Sync": "Sincronitzar",
     "System": "Sistema",
     "TvShows": "Sèries de TV",
@@ -82,11 +82,11 @@
     "UserCreatedWithName": "S'ha creat l'usuari {0}",
     "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
     "UserDownloadingItemWithValues": "{0} està descarregant {1}",
-    "UserLockedOutWithName": "L'usuari {0} ha sigut tancat",
+    "UserLockedOutWithName": "L'usuari {0} ha sigut expulsat",
     "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
     "UserOnlineFromDevice": "{0} està connectat des de {1}",
     "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
-    "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per {0}",
+    "UserPolicyUpdatedWithName": "La política d'usuari s'ha actualitzat per a {0}",
     "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
     "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
     "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
@@ -94,14 +94,14 @@
     "VersionNumber": "Versió {0}",
     "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
     "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
-    "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'Internet.",
-    "TaskRefreshChannels": "Actualitza Canals",
-    "TaskCleanTranscodeDescription": "Elimina els arxius temporals de transcodificacions que tinguin més d'un dia.",
+    "TaskRefreshChannelsDescription": "Actualitza la informació dels canals d'internet.",
+    "TaskRefreshChannels": "Actualitza els canals",
+    "TaskCleanTranscodeDescription": "Elimina els arxius de transcodificacions que tinguin més d'un dia.",
     "TaskCleanTranscode": "Neteja les transcodificacions",
-    "TaskUpdatePluginsDescription": "Actualitza les extensions que estan configurades per actualitzar-se automàticament.",
-    "TaskUpdatePlugins": "Actualitza les extensions",
+    "TaskUpdatePluginsDescription": "Actualitza els connectors que estan configurats per a actualitzar-se automàticament.",
+    "TaskUpdatePlugins": "Actualitza els connectors",
     "TaskRefreshPeopleDescription": "Actualitza les metadades dels actors i directors de la teva mediateca.",
-    "TaskRefreshPeople": "Actualitza Persones",
+    "TaskRefreshPeople": "Actualitza les persones",
     "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
     "TaskCleanLogs": "Neteja els registres",
     "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
@@ -110,12 +110,12 @@
     "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
     "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
     "TaskCleanCache": "Elimina arxius temporals",
-    "TasksChannelsCategory": "Canals d'Internet",
+    "TasksChannelsCategory": "Canals d'internet",
     "TasksApplicationCategory": "Aplicació",
     "TasksLibraryCategory": "Biblioteca",
     "TasksMaintenanceCategory": "Manteniment",
     "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
-    "TaskCleanActivityLog": "Buidar Registre d'Activitat",
+    "TaskCleanActivityLog": "Buidar el registre d'activitat",
     "Undefined": "Indefinit",
     "Forced": "Forçat",
     "Default": "Per defecte",
@@ -124,5 +124,5 @@
     "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.",
     "TaskKeyframeExtractor": "Extractor de fotogrames clau",
     "External": "Extern",
-    "HearingImpaired": "Discapacitat Auditiva"
+    "HearingImpaired": "Discapacitat auditiva"
 }

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

@@ -28,7 +28,7 @@
     "NameSeasonNumber": "Tymor {0}",
     "MusicVideos": "Fideos Cerddoriaeth",
     "MixedContent": "Cynnwys amrywiol",
-    "HomeVideos": "Fideos Cartref",
+    "HomeVideos": "Genres",
     "HeaderNextUp": "Nesaf i Fyny",
     "HeaderFavoriteArtists": "Ffefryn Artistiaid",
     "HeaderFavoriteAlbums": "Ffefryn Albwmau",
@@ -122,5 +122,6 @@
     "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
     "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
     "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
-    "TaskCleanCache": "Gwaghau Ffolder Cache"
+    "TaskCleanCache": "Gwaghau Ffolder Cache",
+    "HearingImpaired": "Nam ar y clyw"
 }

+ 52 - 52
Emby.Server.Implementations/Localization/Core/da.json

@@ -1,9 +1,9 @@
 {
-    "Albums": "Albummer",
+    "Albums": "Album",
     "AppDeviceValues": "App: {0}, Enhed: {1}",
     "Application": "Applikation",
     "Artists": "Kunstnere",
-    "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret",
+    "AuthenticationSucceededWithUserName": "{0} er logget ind",
     "Books": "Bøger",
     "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
     "Channels": "Kanaler",
@@ -11,17 +11,17 @@
     "Collections": "Samlinger",
     "DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
     "DeviceOnlineWithName": "{0} er forbundet",
-    "FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}",
+    "FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
     "Favorites": "Favoritter",
     "Folders": "Mapper",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Albumkunstner",
+    "HeaderAlbumArtists": "Albums kunstnere",
     "HeaderContinueWatching": "Fortsæt afspilning",
-    "HeaderFavoriteAlbums": "Favoritalbummer",
-    "HeaderFavoriteArtists": "Favoritkunstnere",
-    "HeaderFavoriteEpisodes": "Favoritepisoder",
-    "HeaderFavoriteShows": "Favoritserier",
-    "HeaderFavoriteSongs": "Favoritsange",
+    "HeaderFavoriteAlbums": "Favorit albummer",
+    "HeaderFavoriteArtists": "Favorit kunstnere",
+    "HeaderFavoriteEpisodes": "Favorit afsnit",
+    "HeaderFavoriteShows": "Favorit serier",
+    "HeaderFavoriteSongs": "Favorit sange",
     "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Næste",
     "HeaderRecordingGroups": "Optagelsesgrupper",
@@ -39,90 +39,90 @@
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Music": "Musik",
-    "MusicVideos": "Musik videoer",
+    "MusicVideos": "Musikvideoer",
     "NameInstallFailed": "{0} installationen mislykkedes",
     "NameSeasonNumber": "Sæson {0}",
     "NameSeasonUnknown": "Ukendt sæson",
-    "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig til download.",
-    "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikation tilgængelig",
-    "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikation installeret",
+    "NewVersionIsAvailable": "En ny version af Jellyfin Server er tilgængelig.",
+    "NotificationOptionApplicationUpdateAvailable": "Opdatering til applikationen er tilgængelig",
+    "NotificationOptionApplicationUpdateInstalled": "Opdatering til applikationen blev installeret",
     "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
     "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
-    "NotificationOptionInstallationFailed": "Installationen fejlede",
+    "NotificationOptionInstallationFailed": "Installationen mislykkedes",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
-    "NotificationOptionPluginError": "Pluginfejl",
-    "NotificationOptionPluginInstalled": "Plugin installeret",
-    "NotificationOptionPluginUninstalled": "Plugin afinstalleret",
-    "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin installeret",
-    "NotificationOptionServerRestartRequired": "Genstart af server påkrævet",
-    "NotificationOptionTaskFailed": "Planlagt opgave fejlet",
-    "NotificationOptionUserLockedOut": "Bruger låst ude",
+    "NotificationOptionPluginError": "Plugin fejl",
+    "NotificationOptionPluginInstalled": "Plugin blev installeret",
+    "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
+    "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
+    "NotificationOptionServerRestartRequired": "Genstart af serveren er påkrævet",
+    "NotificationOptionTaskFailed": "Planlagt opgave er fejlet",
+    "NotificationOptionUserLockedOut": "Bruger er låst ude",
     "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
-    "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
-    "Photos": "Fotoer",
+    "NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
+    "Photos": "Fotos",
     "Playlists": "Afspilningslister",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} blev installeret",
     "PluginUninstalledWithName": "{0} blev afinstalleret",
     "PluginUpdatedWithName": "{0} blev opdateret",
     "ProviderValue": "Udbyder: {0}",
-    "ScheduledTaskFailedWithName": "{0} fejlet",
-    "ScheduledTaskStartedWithName": "{0} påbegyndt",
+    "ScheduledTaskFailedWithName": "{0} mislykkedes",
+    "ScheduledTaskStartedWithName": "{0} påbegyndte",
     "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
     "Shows": "Serier",
     "Songs": "Sange",
-    "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte op. Prøv venligst igen om lidt.",
+    "StartupEmbyServerIsLoading": "Jellyfin Server er i gang med at starte. Forsøg igen om et øjeblik.",
     "SubtitleDownloadFailureForItem": "Fejlet i download af undertekster for {0}",
-    "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke downloades fra {0} til {1}",
-    "Sync": "Synk",
+    "SubtitleDownloadFailureFromForItem": "Undertekster kunne ikke hentes fra {0} til {1}",
+    "Sync": "Synkroniser",
     "System": "System",
-    "TvShows": "Tv-serier",
+    "TvShows": "TV-serier",
     "User": "Bruger",
     "UserCreatedWithName": "Bruger {0} er blevet oprettet",
-    "UserDeletedWithName": "Brugeren {0} er blevet slettet",
-    "UserDownloadingItemWithValues": "{0} downloader {1}",
+    "UserDeletedWithName": "Brugeren {0} er nu slettet",
+    "UserDownloadingItemWithValues": "{0} henter {1}",
     "UserLockedOutWithName": "Brugeren {0} er blevet låst ude",
     "UserOfflineFromDevice": "{0} har afbrudt fra {1}",
     "UserOnlineFromDevice": "{0} er online fra {1}",
-    "UserPasswordChangedWithName": "Adgangskode er ændret for bruger {0}",
-    "UserPolicyUpdatedWithName": "Brugerpolitik er blevet opdateret for {0}",
+    "UserPasswordChangedWithName": "Adgangskode er ændret for brugeren {0}",
+    "UserPolicyUpdatedWithName": "Brugerpolitikken er blevet opdateret for {0}",
     "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
     "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfiguration.",
-    "TaskDownloadMissingSubtitles": "Download manglende undertekster",
-    "TaskUpdatePluginsDescription": "Downloader og installere opdateringer for plugins som er konfigureret til at opdatere automatisk.",
+    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+    "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
+    "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
     "TaskUpdatePlugins": "Opdater Plugins",
-    "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gammle.",
-    "TaskCleanLogs": "Ryd Log Mappe",
-    "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdaterer metadata.",
+    "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
+    "TaskCleanLogs": "Ryd Log mappe",
+    "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
     "TaskRefreshLibrary": "Scan Medie Bibliotek",
-    "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke har brug for længere.",
-    "TaskCleanCache": "Ryd Cache Mappe",
+    "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
+    "TaskCleanCache": "Ryd Cache mappe",
     "TasksChannelsCategory": "Internet Kanaler",
     "TasksApplicationCategory": "Applikation",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedligeholdelse",
-    "TaskRefreshChapterImages": "Udtræk Kapitel billeder",
+    "TaskRefreshChapterImages": "Udtræk kapitel billeder",
     "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
-    "TaskRefreshChannelsDescription": "Genopfrisker internet kanal information.",
-    "TaskRefreshChannels": "Genopfrisk Kanaler",
-    "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end en dag gammel.",
-    "TaskCleanTranscode": "Rengør Transcode Mappen",
-    "TaskRefreshPeople": "Genopfrisk Personer",
-    "TaskRefreshPeopleDescription": "Opdatere metadata for skuespillere og instruktører i dit bibliotek.",
-    "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigureret alder.",
+    "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+    "TaskRefreshChannels": "Opdater Kanaler",
+    "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
+    "TaskCleanTranscode": "Tøm Transcode mappen",
+    "TaskRefreshPeople": "Opdater Personer",
+    "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
+    "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
     "TaskCleanActivityLog": "Ryd Aktivitetslog",
     "Undefined": "Udefineret",
     "Forced": "Tvunget",
     "Default": "Standard",
-    "TaskOptimizeDatabaseDescription": "Kompakter database og forkorter fri plads. Ved at køre denne proces efter at scanne biblioteket eller efter at ændre noget som kunne have indflydelse på databasen, kan forbedre ydeevne.",
+    "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
     "TaskOptimizeDatabase": "Optimér database",
-    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan godt tage lang tid.",
-    "TaskKeyframeExtractor": "Billedramme udtrækker",
+    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
+    "TaskKeyframeExtractor": "Nøglebillede udtræk",
     "External": "Ekstern",
     "HearingImpaired": "Hørehæmmet"
 }

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

@@ -118,11 +118,11 @@
     "TaskCleanActivityLog": "Borrar log de actividades",
     "Undefined": "Indefinido",
     "Forced": "Forzado",
-    "Default": "Por Defecto",
+    "Default": "Predeterminado",
     "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
     "TaskOptimizeDatabase": "Optimización de base de datos",
     "External": "Externo",
     "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
     "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
-    "HearingImpaired": "Personas con discapacidad auditiva"
+    "HearingImpaired": "Discapacidad Auditiva"
 }

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

@@ -31,7 +31,7 @@
     "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
     "LabelIpAddressValue": "Dirección IP: {0}",
     "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
-    "Latest": "Últimos",
+    "Latest": "Últimas",
     "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
     "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "La sección {0} de configuración del servidor ha sido actualizada",

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

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

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

@@ -123,5 +123,6 @@
     "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
     "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
     "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
-    "External": "خارجی"
+    "External": "خارجی",
+    "HearingImpaired": "مشکل شنوایی"
 }

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

@@ -118,7 +118,7 @@
     "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
     "TaskCleanActivityLog": "Tyhjennä toimintahistoria",
     "Undefined": "Määrittelemätön",
-    "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.",
+    "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastopäivityksen tai muiden mahdollisten tietokantamuutosten jälkeen voi parantaa suorituskykyä.",
     "TaskOptimizeDatabase": "Optimoi tietokanta",
     "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
     "TaskKeyframeExtractor": "Avainkuvien purkain",

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

@@ -119,5 +119,9 @@
     "Undefined": "Hindi tiyak",
     "Forced": "Sapilitan",
     "TaskOptimizeDatabaseDescription": "Iko-compact ang database at ita-truncate ang free space. Ang pagpapatakbo ng gawaing ito pagkatapos ng pag-scan sa library o paggawa ng iba pang mga pagbabago na nagpapahiwatig ng mga pagbabago sa database ay maaaring magpa-improve ng performance.",
-    "TaskOptimizeDatabase": "I-optimize ang database"
+    "TaskOptimizeDatabase": "I-optimize ang database",
+    "HearingImpaired": "Bingi",
+    "TaskKeyframeExtractor": "Tagabunot ng Keyframe",
+    "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
+    "External": "External"
 }

+ 8 - 4
Emby.Server.Implementations/Localization/Core/gsw.json

@@ -1,7 +1,7 @@
 {
     "Albums": "Alben",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
-    "Application": "Anwendung",
+    "Application": "Applikation",
     "Artists": "Künstler",
     "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
     "Books": "Bücher",
@@ -14,7 +14,7 @@
     "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
     "Favorites": "Favoriten",
     "Folders": "Ordner",
-    "Genres": "Genres",
+    "Genres": "Genre",
     "HeaderAlbumArtists": "Album-Künstler",
     "HeaderContinueWatching": "weiter schauen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
     "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
     "NotificationOptionCameraImageUploaded": "Foti ueglade",
-    "NotificationOptionInstallationFailed": "Installationsfehler",
+    "NotificationOptionInstallationFailed": "Installationsfähler",
     "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
     "NotificationOptionPluginError": "Plugin-Fäuer",
     "NotificationOptionPluginInstalled": "Plugin installiert",
@@ -120,5 +120,9 @@
     "Forced": "Erzwungen",
     "Default": "Standard",
     "TaskOptimizeDatabase": "Datenbank optimieren",
-    "External": "Extern"
+    "External": "Extern",
+    "TaskOptimizeDatabaseDescription": "Kompromiert d Datenbank und trennt freie Speicherplatz. Durch die Ufagb cha d Leistig nach em ne Scan vor Bibliothek oder andere Ufgabe verbesseret werde.",
+    "HearingImpaired": "Hörgschädigti",
+    "TaskKeyframeExtractor": "Keyframe-Extraktor",
+    "TaskKeyframeExtractorDescription": "Extrahiert Keyframes us Videodateien zum erstelle vo genauere HLS Playliste. Die Ufgab cha für e langi Zyt laufe."
 }

+ 57 - 1
Emby.Server.Implementations/Localization/Core/hi.json

@@ -67,5 +67,61 @@
     "Plugin": "प्लग-इन",
     "Playlists": "प्लेलिस्ट",
     "Photos": "तस्वीरें",
-    "External": "बाहरी"
+    "External": "बाहरी",
+    "PluginUpdatedWithName": "{0} अपडेट हुए",
+    "ScheduledTaskStartedWithName": "{0} शुरू हुए",
+    "Songs": "गाने",
+    "UserStartedPlayingItemWithValues": "{0} {2} पर {1} खेल रहे हैं",
+    "UserStoppedPlayingItemWithValues": "{0} ने {2} पर {1} खेलना खत्म किया",
+    "StartupEmbyServerIsLoading": "जेलीफ़िन सर्वर लोड हो रहा है। कृपया शीघ्र ही पुन: प्रयास करें।",
+    "ServerNameNeedsToBeRestarted": "{0} रीस्टार्ट करने की आवश्यकता है",
+    "UserCreatedWithName": "उपयोगकर्ता {0} बनाया गया",
+    "UserDownloadingItemWithValues": "{0} डाउनलोड हो रहा है",
+    "UserOfflineFromDevice": "{0} {1} से डिस्कनेक्ट हो गया है",
+    "Undefined": "अनिर्धारित",
+    "UserOnlineFromDevice": "{0} {1} से ऑनलाइन है",
+    "Shows": "शो",
+    "UserPasswordChangedWithName": "उपयोगकर्ता {0} के लिए पासवर्ड बदल दिया गया है",
+    "UserDeletedWithName": "उपयोगकर्ता {0} हटा दिया गया",
+    "UserPolicyUpdatedWithName": "{0} के लिए उपयोगकर्ता नीति अपडेट कर दी गई है",
+    "User": "उपयोगकर्ता",
+    "SubtitleDownloadFailureFromForItem": "{1} के लिए {0} से उपशीर्षक डाउनलोड करने में विफल",
+    "ProviderValue": "प्रदाता: {0}",
+    "ScheduledTaskFailedWithName": "{0}असफल",
+    "UserLockedOutWithName": "उपयोगकर्ता {0} को लॉक आउट कर दिया गया है",
+    "System": "प्रणाली",
+    "TvShows": "टीवी शो",
+    "HearingImpaired": "मूक बधिर",
+    "ValueSpecialEpisodeName": "विशेष - {0}",
+    "TasksMaintenanceCategory": "रखरखाव",
+    "Sync": "समाकलयति",
+    "VersionNumber": "{0} पाठान्तर",
+    "ValueHasBeenAddedToLibrary": "{0} आपके माध्यम ग्रन्थालय में उपजात हो गया हैं",
+    "TasksLibraryCategory": "संग्रहालय",
+    "TaskOptimizeDatabase": "जानकारी प्रवृद्धि",
+    "TaskDownloadMissingSubtitles": "असमेत अनुलेख को अवाहरति करें",
+    "TaskRefreshLibrary": "माध्यम संग्राहत को छाने",
+    "TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें",
+    "TasksChannelsCategory": "इंटरनेट प्रणाली",
+    "TasksApplicationCategory": "अनुप्रयोग",
+    "TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें",
+    "TaskKeyframeExtractor": "कीफ़्रेम एक्सट्रैक्टर",
+    "TaskCleanActivityLogDescription": "कॉन्फ़िगर की गई आयु से पुरानी गतिविधि लॉग प्रविष्टियां हटाता है।",
+    "TaskRefreshChapterImagesDescription": "अध्याय वाले वीडियो के लिए थंबनेल बनाता है।",
+    "TaskRefreshLibraryDescription": "नई फ़ाइलों के लिए आपकी मीडिया लाइब्रेरी को स्कैन करता है और मेटाडेटा को ताज़ा करता है।",
+    "TaskCleanLogs": "स्वच्छ लॉग निर्देशिका",
+    "TaskUpdatePluginsDescription": "प्लगइन्स के लिए अपडेट डाउनलोड और इंस्टॉल करें जो स्वचालित रूप से अपडेट करने के लिए कॉन्फ़िगर किए गए हैं।",
+    "TaskCleanTranscode": "स्वच्छ ट्रांसकोड निर्देशिका",
+    "TaskCleanTranscodeDescription": "एक दिन से अधिक पुरानी ट्रांसकोड फ़ाइलें हटाता है.",
+    "TaskRefreshChannelsDescription": "इंटरनेट चैनल की जानकारी को ताज़ा करता है।",
+    "TaskOptimizeDatabaseDescription": "डेटाबेस को कॉम्पैक्ट करता है और मुक्त स्थान को छोटा करता है। लाइब्रेरी को स्कैन करने के बाद इस कार्य को चलाने या अन्य परिवर्तन करने से जो डेटाबेस संशोधनों को लागू करते हैं, प्रदर्शन में सुधार कर सकते हैं।",
+    "TaskRefreshChannels": "इंटरनेट चैनल की जानकारी को ताज़ा करता है",
+    "TaskRefreshChapterImages": "अध्याय छवियाँ निकालें",
+    "TaskCleanLogsDescription": "{0} दिन से अधिक पुरानी लॉग फ़ाइलें हटाता है।",
+    "TaskCleanCacheDescription": "उन कैश फ़ाइलों को हटाता है जिनकी अब सिस्टम को आवश्यकता नहीं है।",
+    "TaskUpdatePlugins": "अद्यतन प्लगइन्स",
+    "TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।",
+    "TaskCleanCache": "स्वच्छ कैश निर्देशिका",
+    "TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
+    "TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
 }

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

@@ -82,7 +82,7 @@
     "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
     "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
     "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
-    "CameraImageUploadedFrom": "Gambar kamera baru telah diunggah dari {0}",
+    "CameraImageUploadedFrom": "Sebuah gambar kamera baru telah diunggah dari {0}",
     "DeviceOfflineWithName": "{0} telah terputus",
     "DeviceOnlineWithName": "{0} telah terhubung",
     "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",

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

@@ -107,5 +107,14 @@
     "TasksApplicationCategory": "Forrit",
     "TasksLibraryCategory": "Miðlasafn",
     "TasksMaintenanceCategory": "Viðhald",
-    "Default": "Sjálfgefið"
+    "Default": "Sjálfgefið",
+    "TaskCleanActivityLog": "Hreinsa athafnaskrá",
+    "TaskRefreshPeople": "Endurnýja fólk",
+    "TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
+    "TaskOptimizeDatabase": "Fínstilla gagnagrunn",
+    "Undefined": "Óskilgreint",
+    "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
+    "TaskCleanLogs": "Hreinsa færslu skrá",
+    "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
+    "HearingImpaired": "Heyrnarskertur"
 }

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно