فهرست منبع

Merge branch 'jellyfin:master' into master

Ronan Charles-Lorel 1 سال پیش
والد
کامیت
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)
     displayName: Set release version (stable)
     condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/v')
     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'
     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)'
   - 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
       label: Jellyfin Version
       description: What version of Jellyfin are you running?
       description: What version of Jellyfin are you running?
       options:
       options:
-        - 10.8.0
+        - 10.8.z
+        - 10.8.9
         - 10.7.7
         - 10.7.7
-        - 10.7.z
         - 10.6.4
         - 10.6.4
         - Other
         - Other
     validations:
     validations:
@@ -47,13 +47,15 @@ body:
       label: Environment
       label: Environment
       description: |
       description: |
         Examples:
         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]
         - **Virtualization**: [e.g. Docker, KVM, LXC]
         - **Clients**: [Browser, Android, Fire Stick, etc.]
         - **Clients**: [Browser, Android, Fire Stick, etc.]
         - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
         - **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]
         - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
         - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
         - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+        - **GPU Model**: [e.g. none, UHD630, GTX1050, etc.]
         - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
         - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
         - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
         - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
         - **Base URL**: [e.g. none, yes: /example]
         - **Base URL**: [e.g. none, yes: /example]
@@ -61,12 +63,14 @@ body:
         - **Storage**: [e.g. local, NFS, cloud]
         - **Storage**: [e.g. local, NFS, cloud]
       value: |
       value: |
         - OS:
         - OS:
+        - Linux Kernel:
         - Virtualization:
         - Virtualization:
         - Clients:
         - Clients:
         - Browser:
         - Browser:
         - FFmpeg Version:
         - FFmpeg Version:
         - Playback Method:
         - Playback Method:
         - Hardware Acceleration:
         - Hardware Acceleration:
+        - GPU Model:
         - Plugins:
         - Plugins:
         - Reverse Proxy:
         - Reverse Proxy:
         - Base URL:
         - Base URL:
@@ -84,8 +88,8 @@ body:
     id: ffmpeg-logs
     id: ffmpeg-logs
     attributes:
     attributes:
       label: FFmpeg logs
       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
       render: shell
   - type: textarea
   - type: textarea
     id: browserlogs
     id: browserlogs

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

@@ -19,6 +19,7 @@ jobs:
         if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
         if: ${{ github.event_name == 'push' || github.event_name == 'pull_request_target'}}
         with:
         with:
           dirtyLabel: 'merge conflict'
           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 }}
           repoToken: ${{ secrets.JF_BOT_TOKEN }}
 
 
   project:
   project:

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

@@ -20,18 +20,18 @@ jobs:
 
 
     steps:
     steps:
     - name: Checkout repository
     - name: Checkout repository
-      uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+      uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
     - name: Setup .NET
     - name: Setup .NET
-      uses: actions/setup-dotnet@607fce577a46308457984d59e4954e075820f10a # tag=v3
+      uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
       with:
       with:
         dotnet-version: '7.0.x'
         dotnet-version: '7.0.x'
 
 
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+      uses: github/codeql-action/init@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         queries: +security-extended
         queries: +security-extended
     - name: Autobuild
     - name: Autobuild
-      uses: github/codeql-action/autobuild@3ebbd71c74ef574dbc558c82f70e52732c8b44fe # v2
+      uses: github/codeql-action/autobuild@f6e388ebf0efc915c6c5b165b019ee61a6746a38 # v2.20.1
     - name: Perform CodeQL Analysis
     - 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
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Notify as seen
       - 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:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           comment-id: ${{ github.event.comment.id }}
           comment-id: ${{ github.event.comment.id }}
           reactions: '+1'
           reactions: '+1'
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0
@@ -43,7 +43,7 @@ jobs:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     steps:
     steps:
       - name: Notify as seen
       - name: Notify as seen
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ github.event.comment != null }}
         if: ${{ github.event.comment != null }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -51,14 +51,14 @@ jobs:
           reactions: eyes
           reactions: eyes
 
 
       - name: Checkout the latest code
       - name: Checkout the latest code
-        uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
           fetch-depth: 0
 
 
       - name: Notify as running
       - name: Notify as running
         id: comment_running
         id: comment_running
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ github.event.comment != null }}
         if: ${{ github.event.comment != null }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -93,7 +93,7 @@ jobs:
           exit ${retcode}
           exit ${retcode}
 
 
       - name: Notify with result success
       - name: Notify with result success
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ github.event.comment != null && success() }}
         if: ${{ github.event.comment != null && success() }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}
@@ -108,7 +108,7 @@ jobs:
           reactions: hooray
           reactions: hooray
 
 
       - name: Notify with result failure
       - name: Notify with result failure
-        uses: peter-evans/create-or-update-comment@5adcb0bb0f9fb3f95ef05400558bdb3f329ee808 # tag=v2
+        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
         if: ${{ github.event.comment != null && failure() }}
         if: ${{ github.event.comment != null && failure() }}
         with:
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           token: ${{ secrets.JF_BOT_TOKEN }}

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

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

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

@@ -1,4 +1,4 @@
-name: Issue Stale Check
+name: Stale Check
 
 
 on:
 on:
   schedule:
   schedule:
@@ -7,12 +7,15 @@ on:
 
 
 permissions:
 permissions:
   issues: write
   issues: write
+  pull-requests: write
+
 jobs:
 jobs:
-  stale:
+  issues:
+    name: Check issues
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
     steps:
-      - uses: actions/stale@6f05e4244c9a0b2ed3401882b05d701dd0a7289b # v7
+      - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
         with:
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
           days-before-stale: 120
           days-before-stale: 120
@@ -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.
             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).
             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)
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [ikomhoog](https://github.com/ikomhoog)
  - [ikomhoog](https://github.com/ikomhoog)
  - [jftuga](https://github.com/jftuga)
  - [jftuga](https://github.com/jftuga)
+ - [jmshrv](https://github.com/jmshrv)
  - [joern-h](https://github.com/joern-h)
  - [joern-h](https://github.com/joern-h)
  - [joshuaboniface](https://github.com/joshuaboniface)
  - [joshuaboniface](https://github.com/joshuaboniface)
  - [JustAMan](https://github.com/JustAMan)
  - [JustAMan](https://github.com/JustAMan)
@@ -125,6 +126,7 @@
  - [SuperSandro2000](https://github.com/SuperSandro2000)
  - [SuperSandro2000](https://github.com/SuperSandro2000)
  - [tbraeutigam](https://github.com/tbraeutigam)
  - [tbraeutigam](https://github.com/tbraeutigam)
  - [teacupx](https://github.com/teacupx)
  - [teacupx](https://github.com/teacupx)
+ - [TelepathicWalrus](https://github.com/TelepathicWalrus)
  - [Terror-Gene](https://github.com/Terror-Gene)
  - [Terror-Gene](https://github.com/Terror-Gene)
  - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
  - [ThatNerdyPikachu](https://github.com/ThatNerdyPikachu)
  - [ThibaultNocchi](https://github.com/ThibaultNocchi)
  - [ThibaultNocchi](https://github.com/ThibaultNocchi)
@@ -162,6 +164,8 @@
  - [vgambier](https://github.com/vgambier)
  - [vgambier](https://github.com/vgambier)
  - [MinecraftPlaye](https://github.com/MinecraftPlaye)
  - [MinecraftPlaye](https://github.com/MinecraftPlaye)
  - [RealGreenDragon](https://github.com/RealGreenDragon)
  - [RealGreenDragon](https://github.com/RealGreenDragon)
+ - [ipitio](https://github.com/ipitio)
+ - [TheTyrius](https://github.com/TheTyrius)
 
 
 # Emby Contributors
 # Emby Contributors
 
 
@@ -231,3 +235,4 @@
  - [Matthew Jones](https://github.com/matthew-jones-uk)
  - [Matthew Jones](https://github.com/matthew-jones-uk)
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Utku Özdemir](https://github.com/utkuozdemir)
  - [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 - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
  && mv dist /dist
 
 
 FROM debian:stable-slim as app
 FROM debian:stable-slim as app
@@ -37,7 +38,7 @@ RUN apt-get update \
  && apt-get update \
  && apt-get update \
  && apt-get install --no-install-recommends --no-install-suggests -y \
  && apt-get install --no-install-recommends --no-install-suggests -y \
    mesa-va-drivers \
    mesa-va-drivers \
-   jellyfin-ffmpeg \
+   jellyfin-ffmpeg5 \
    openssl \
    openssl \
    locales \
    locales \
 # Intel VAAPI Tone mapping dependencies:
 # 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 - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
  && mv dist /dist
 
 
 FROM multiarch/qemu-user-static:x86_64-arm as qemu
 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 - \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
  && cd jellyfin-web-* \
  && cd jellyfin-web-* \
  && npm ci --no-audit --unsafe-perm \
  && npm ci --no-audit --unsafe-perm \
+ && npm run build:production \
  && mv dist /dist
  && mv dist /dist
 
 
 FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu
 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>
         /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         private static IEnumerable<StateVariable> GetStateVariables()
         {
         {
-            var list = new List<StateVariable>
+            return new StateVariable[]
             {
             {
                 new StateVariable
                 new StateVariable
                 {
                 {
@@ -114,8 +114,6 @@ namespace Emby.Dlna.ConnectionManager
                     SendsEvents = false
                     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>
         /// <returns>The <see cref="IEnumerable{StateVariable}"/>.</returns>
         private static IEnumerable<StateVariable> GetStateVariables()
         private static IEnumerable<StateVariable> GetStateVariables()
         {
         {
-            var list = new List<StateVariable>
+            return new StateVariable[]
             {
             {
                 new StateVariable
                 new StateVariable
                 {
                 {
@@ -154,8 +154,6 @@ namespace Emby.Dlna.ContentDirectory
                     SendsEvents = false
                     SendsEvents = false
                 }
                 }
             };
             };
-
-            return list;
         }
         }
     }
     }
 }
 }

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

@@ -10,6 +10,7 @@ using System.Text;
 using System.Xml;
 using System.Xml;
 using Emby.Dlna.ContentDirectory;
 using Emby.Dlna.ContentDirectory;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -870,11 +871,11 @@ namespace Emby.Dlna.Didl
 
 
             var types = new[]
             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
             // 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)
             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-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
     </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>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -80,7 +80,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Http" />
   </ItemGroup>
   </ItemGroup>
 
 
 </Project>
 </Project>

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

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

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

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

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

@@ -2,9 +2,11 @@
 
 
 using System;
 using System;
 using System.Globalization;
 using System.Globalization;
+using System.IO;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Mime;
 using System.Net.Mime;
 using System.Text;
 using System.Text;
+using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using System.Xml;
 using System.Xml;
@@ -15,7 +17,10 @@ using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Dlna.PlayTo
 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 ILogger _logger;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly IHttpClientFactory _httpClientFactory;
@@ -44,25 +49,44 @@ namespace Emby.Dlna.PlayTo
 
 
         private async Task<XDocument?> SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
         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();
             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
             try
             {
             {
                 return await XDocument.LoadAsync(
                 return await XDocument.LoadAsync(
-                    stream,
+                    ms,
                     LoadOptions.None,
                     LoadOptions.None,
                     cancellationToken).ConfigureAwait(false);
                     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
             // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
             return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
             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
 #pragma warning disable CS1591
 
 
 using System;
 using System;
@@ -66,7 +64,8 @@ namespace Emby.Dlna.PlayTo
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             ILocalizationManager localization,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
             IMediaSourceManager mediaSourceManager,
-            IMediaEncoder mediaEncoder)
+            IMediaEncoder mediaEncoder,
+            Device device)
         {
         {
             _session = session;
             _session = session;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
@@ -82,14 +81,7 @@ namespace Emby.Dlna.PlayTo
             _localization = localization;
             _localization = localization;
             _mediaSourceManager = mediaSourceManager;
             _mediaSourceManager = mediaSourceManager;
             _mediaEncoder = mediaEncoder;
             _mediaEncoder = mediaEncoder;
-        }
-
-        public bool IsSessionActive => !_disposed && _device is not null;
 
 
-        public bool SupportsMediaControl => IsSessionActive;
-
-        public void Init(Device device)
-        {
             _device = device;
             _device = device;
             _device.OnDeviceUnavailable = OnDeviceUnavailable;
             _device.OnDeviceUnavailable = OnDeviceUnavailable;
             _device.PlaybackStart += OnDevicePlaybackStart;
             _device.PlaybackStart += OnDevicePlaybackStart;
@@ -102,6 +94,10 @@ namespace Emby.Dlna.PlayTo
             _deviceDiscovery.DeviceLeft += OnDeviceDiscoveryDeviceLeft;
             _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.
          * 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;
             var info = e.Argument;
 
 
             if (!_disposed
             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(_device.Properties.UUID, StringComparison.OrdinalIgnoreCase) != -1
                 && (usn.IndexOf("MediaRenderer:", 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)))
                         && nt.IndexOf("MediaRenderer:", StringComparison.OrdinalIgnoreCase) != -1)))
             {
             {
                 OnDeviceUnavailable();
                 OnDeviceUnavailable();
             }
             }
         }
         }
 
 
-        private async void OnDeviceMediaChanged(object sender, MediaChangedEventArgs e)
+        private async void OnDeviceMediaChanged(object? sender, MediaChangedEventArgs e)
         {
         {
             if (_disposed || string.IsNullOrEmpty(e.OldMediaInfo.Url))
             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)
             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)
             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)
             if (_disposed)
             {
             {
@@ -486,9 +482,9 @@ namespace Emby.Dlna.PlayTo
 
 
         private PlaylistItem CreatePlaylistItem(
         private PlaylistItem CreatePlaylistItem(
             BaseItem item,
             BaseItem item,
-            User user,
+            User? user,
             long startPostionTicks,
             long startPostionTicks,
-            string mediaSourceId,
+            string? mediaSourceId,
             int? audioStreamIndex,
             int? audioStreamIndex,
             int? subtitleStreamIndex)
             int? subtitleStreamIndex)
         {
         {
@@ -525,7 +521,7 @@ namespace Emby.Dlna.PlayTo
             return playlistItem;
             return playlistItem;
         }
         }
 
 
-        private string GetDlnaHeaders(PlaylistItem item)
+        private string? GetDlnaHeaders(PlaylistItem item)
         {
         {
             var profile = item.Profile;
             var profile = item.Profile;
             var streamInfo = item.StreamInfo;
             var streamInfo = item.StreamInfo;
@@ -579,7 +575,7 @@ namespace Emby.Dlna.PlayTo
             return null;
             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))
             if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase))
             {
             {
@@ -696,7 +692,6 @@ namespace Emby.Dlna.PlayTo
             _device.MediaChanged -= OnDeviceMediaChanged;
             _device.MediaChanged -= OnDeviceMediaChanged;
             _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
             _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
             _device.OnDeviceUnavailable = null;
             _device.OnDeviceUnavailable = null;
-            _device = null;
 
 
             _disposed = true;
             _disposed = true;
         }
         }
@@ -716,7 +711,7 @@ namespace Emby.Dlna.PlayTo
                 case GeneralCommandType.ToggleMute:
                 case GeneralCommandType.ToggleMute:
                     return _device.ToggleMute(cancellationToken);
                     return _device.ToggleMute(cancellationToken);
                 case GeneralCommandType.SetAudioStreamIndex:
                 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))
                         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");
                     throw new ArgumentException("SetSubtitleStreamIndex argument cannot be null");
                 case GeneralCommandType.SetVolume:
                 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))
                         if (int.TryParse(vol, NumberStyles.Integer, CultureInfo.InvariantCulture, out var volume))
                         {
                         {
@@ -865,34 +860,19 @@ namespace Emby.Dlna.PlayTo
                 throw new ObjectDisposedException(GetType().Name);
                 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 class StreamParams
         {
         {
-            private MediaSourceInfo _mediaSource;
-            private IMediaSourceManager _mediaSourceManager;
+            private MediaSourceInfo? _mediaSource;
+            private IMediaSourceManager? _mediaSourceManager;
 
 
             public Guid ItemId { get; set; }
             public Guid ItemId { get; set; }
 
 
@@ -904,17 +884,17 @@ namespace Emby.Dlna.PlayTo
 
 
             public int? SubtitleStreamIndex { get; set; }
             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)
                 if (_mediaSource is not null)
                 {
                 {
@@ -944,8 +924,8 @@ namespace Emby.Dlna.PlayTo
                 {
                 {
                     var part = parts[i];
                     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))
                         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,
                     _userDataManager,
                     _localization,
                     _localization,
                     _mediaSourceManager,
                     _mediaSourceManager,
-                    _mediaEncoder);
+                    _mediaEncoder,
+                    device);
 
 
                 sessionInfo.AddController(controller);
                 sessionInfo.AddController(controller);
 
 
-                controller.Init(device);
-
                 var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
                 var profile = _dlnaManager.GetProfile(device.Properties.ToDeviceIdentification()) ??
                               _dlnaManager.GetDefaultProfile();
                               _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);
             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;
             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;
             var stateString = string.Empty;
 
 
@@ -150,9 +150,9 @@ namespace Emby.Dlna.PlayTo
                 {
                 {
                     stateString += BuildArgumentXml(arg, "0");
                     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
                 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 = "")
         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))
             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>();
             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)
         private void AppendIconList(StringBuilder builder)

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

@@ -3,6 +3,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 
 
 namespace Emby.Naming.Audio
 namespace Emby.Naming.Audio
 {
 {
@@ -58,13 +59,7 @@ namespace Emby.Naming.Audio
 
 
                 var tmp = trimmedFilename.Slice(prefix.Length).Trim();
                 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;
                     return true;
                 }
                 }

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

@@ -32,7 +32,7 @@ namespace Emby.Naming.AudioBook
             var fileName = Path.GetFileNameWithoutExtension(path);
             var fileName = Path.GetFileNameWithoutExtension(path);
             foreach (var expression in _options.AudioBookPartsExpressions)
             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 (match.Success)
                 {
                 {
                     if (!result.ChapterNumber.HasValue)
                     if (!result.ChapterNumber.HasValue)
@@ -40,7 +40,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["chapter"];
                         var value = match.Groups["chapter"];
                         if (value.Success)
                         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;
                                 result.ChapterNumber = intValue;
                             }
                             }
@@ -52,7 +52,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["part"];
                         var value = match.Groups["part"];
                         if (value.Success)
                         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;
                                 result.PartNumber = intValue;
                             }
                             }

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

@@ -79,25 +79,25 @@ namespace Emby.Naming.AudioBook
                 {
                 {
                     if (group.Count() > 1 || haveChaptersOrPages)
                     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)
                         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
                             else
                             {
                             {
-                                ex.Add(audioFile);
+                                (ex ??= new()).Add(audioFile);
                             }
                             }
                         }
                         }
 
 
-                        if (ex.Count > 0)
+                        if (ex is not null)
                         {
                         {
                             var extra = ex
                             var extra = ex
                                 .OrderBy(x => x.Container)
                                 .OrderBy(x => x.Container)
@@ -108,7 +108,7 @@ namespace Emby.Naming.AudioBook
                             extras.AddRange(extra);
                             extras.AddRange(extra);
                         }
                         }
 
 
-                        if (alt.Count > 0)
+                        if (alt is not null)
                         {
                         {
                             var alternatives = alt
                             var alternatives = alt
                                 .OrderBy(x => x.Container)
                                 .OrderBy(x => x.Container)

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

@@ -30,7 +30,7 @@ namespace Emby.Naming.AudioBook
             AudioBookNameParserResult result = default;
             AudioBookNameParserResult result = default;
             foreach (var expression in _options.AudioBookNamesExpressions)
             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 (match.Success)
                 {
                 {
                     if (result.Name is null)
                     if (result.Name is null)
@@ -47,7 +47,7 @@ namespace Emby.Naming.AudioBook
                         var value = match.Groups["year"];
                         var value = match.Groups["year"];
                         if (value.Success)
                         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;
                                 result.Year = intValue;
                             }
                             }

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

@@ -141,8 +141,7 @@ namespace Emby.Naming.Common
             VideoFileStackingRules = new[]
             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>[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[]
             CleanDateTimes = new[]
@@ -157,7 +156,8 @@ namespace Emby.Naming.Common
                 @"^(?<cleaned>.+?)(\[.*\])",
                 @"^(?<cleaned>.+?)(\[.*\])",
                 @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
                 @"^\s*(?<cleaned>.+?)\WE[0-9]+(-|~)E?[0-9]+(\W|$)",
                 @"^\s*\[[^\]]+\](?!\.\w+$)\s*(?<cleaned>.+)",
                 @"^\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[]
             SubtitleFileExtensions = new[]
@@ -270,7 +270,6 @@ namespace Emby.Naming.Common
                 ".sfx",
                 ".sfx",
                 ".shn",
                 ".shn",
                 ".sid",
                 ".sid",
-                ".spc",
                 ".stm",
                 ".stm",
                 ".strm",
                 ".strm",
                 ".ult",
                 ".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.
                 // so we make sure this one gets tested first.
                 // "Foo Bar 889"
                 // "Foo Bar 889"
                 new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
                 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[]
             VideoExtraRules = new[]
             {
             {
                 new ExtraRule(
                 new ExtraRule(
@@ -797,16 +794,6 @@ namespace Emby.Naming.Common
         /// </summary>
         /// </summary>
         public EpisodeExpression[] EpisodeExpressions { get; set; }
         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>
         /// <summary>
         /// Gets or sets list of video file extensions.
         /// Gets or sets list of video file extensions.
         /// </summary>
         /// </summary>
@@ -877,16 +864,6 @@ namespace Emby.Naming.Common
         /// </summary>
         /// </summary>
         public Regex[] CleanStringRegexes { get; private set; } = Array.Empty<Regex>();
         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>
         /// <summary>
         /// Compiles raw regex strings into regexes.
         /// Compiles raw regex strings into regexes.
         /// </summary>
         /// </summary>
@@ -894,8 +871,6 @@ namespace Emby.Naming.Common
         {
         {
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanDateTimeRegexes = CleanDateTimes.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
             CleanStringRegexes = CleanStrings.Select(Compile).ToArray();
-            EpisodeWithoutSeasonRegexes = EpisodeWithoutSeasonExpressions.Select(Compile).ToArray();
-            EpisodeMultiPartRegexes = EpisodeMultiPartExpressions.Select(Compile).ToArray();
         }
         }
 
 
         private Regex Compile(string exp)
         private Regex Compile(string exp)

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

@@ -42,18 +42,18 @@
   </PropertyGroup>
   </PropertyGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
   </ItemGroup>
   </ItemGroup>
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
     </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>
 
 
 </Project>
 </Project>

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

@@ -113,7 +113,7 @@ namespace Emby.Naming.TV
                     if (expression.DateTimeFormats.Length > 0)
                     if (expression.DateTimeFormats.Length > 0)
                     {
                     {
                         if (DateTime.TryParseExact(
                         if (DateTime.TryParseExact(
-                            match.Groups[0].Value,
+                            match.Groups[0].ValueSpan,
                             expression.DateTimeFormats,
                             expression.DateTimeFormats,
                             CultureInfo.InvariantCulture,
                             CultureInfo.InvariantCulture,
                             DateTimeStyles.None,
                             DateTimeStyles.None,
@@ -125,7 +125,7 @@ namespace Emby.Naming.TV
                             result.Success = true;
                             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.Year = date.Year;
                         result.Month = date.Month;
                         result.Month = date.Month;
@@ -138,12 +138,12 @@ namespace Emby.Naming.TV
                 }
                 }
                 else if (expression.IsNamed)
                 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;
                         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;
                         result.EpisodeNumber = num;
                     }
                     }
@@ -158,7 +158,7 @@ namespace Emby.Naming.TV
                         if (nextIndex >= name.Length
                         if (nextIndex >= name.Length
                             || !"0123456789iIpP".Contains(name[nextIndex], StringComparison.Ordinal))
                             || !"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;
                                 result.EndingEpisodeNumber = num;
                             }
                             }
@@ -170,12 +170,12 @@ namespace Emby.Naming.TV
                 }
                 }
                 else
                 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;
                         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;
                         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
         /// Used for removing separators between words, i.e turns "The_show" into "The show" while
         /// preserving namings like "S.H.O.W".
         /// preserving namings like "S.H.O.W".
         /// </summary>
         /// </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>
         /// <summary>
         /// Resolve information about series from path.
         /// 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.Count == 5
                 && match.Groups[1].Success
                 && match.Groups[1].Success
                 && match.Groups[2].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);
                 result = new CleanDateTimeResult(match.Groups[1].Value.TrimEnd(), year);
                 return true;
                 return true;

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

@@ -56,7 +56,7 @@ namespace Emby.Naming.Video
                 }
                 }
                 else if (rule.RuleType == ExtraRuleType.Regex)
                 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);
                     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>
     /// <param name="isNumerical">Whether the file stack rule uses numerical or alphabetical numbering.</param>
     public FileStackRule(string token, bool isNumerical)
     public FileStackRule(string token, bool isNumerical)
     {
     {
-        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase);
+        _tokenRegex = new Regex(token, RegexOptions.IgnoreCase | RegexOptions.Compiled);
         IsNumerical = isNumerical;
         IsNumerical = isNumerical;
     }
     }
 
 

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

@@ -4,6 +4,7 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Common;
+using Jellyfin.Extensions;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
 
 
 namespace Emby.Naming.Video
 namespace Emby.Naming.Video
@@ -13,6 +14,8 @@ namespace Emby.Naming.Video
     /// </summary>
     /// </summary>
     public static class VideoListResolver
     public static class VideoListResolver
     {
     {
+        private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
         /// <summary>
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
         /// Resolves alternative versions and extras from list of video files.
         /// </summary>
         /// </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]
             // 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++)
             for (var i = 0; i < videos.Count; i++)
             {
             {
                 var video = videos[i];
                 var video = videos[i];
@@ -114,29 +118,43 @@ namespace Emby.Naming.Video
                     continue;
                     continue;
                 }
                 }
 
 
-                if (!IsEligibleForMultiVersion(folderName, video.Files[0].Path, namingOptions))
+                if (!IsEligibleForMultiVersion(folderName, video.Files[0].FileNameWithoutExtension, namingOptions))
                 {
                 {
                     return videos;
                     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>
             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();
             list[0].Name = folderName.ToString();
 
 
             return list;
             return list;
@@ -161,9 +179,8 @@ namespace Emby.Naming.Video
             return true;
             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))
             if (!testFilename.StartsWith(folderName, StringComparison.OrdinalIgnoreCase))
             {
             {
                 return false;
                 return false;
@@ -176,16 +193,15 @@ namespace Emby.Naming.Video
             }
             }
 
 
             // There are no span overloads for regex unfortunately
             // 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.
             // The CleanStringParser should have removed common keywords etc.
-            return string.IsNullOrEmpty(tmpTestFilename)
+            return testFilename.IsEmpty
                    || testFilename[0] == '-'
                    || 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;
                 name = cleanDateTimeResult.Name;
                 year = cleanDateTimeResult.Year;
                 year = cleanDateTimeResult.Year;
 
 
-                if (extraResult.ExtraType is null
-                    && TryCleanString(name, namingOptions, out var newName))
+                if (TryCleanString(name, namingOptions, out var newName))
                 {
                 {
                     name = newName;
                     name = newName;
                 }
                 }

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

@@ -15,7 +15,7 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <ItemGroup>
   <ItemGroup>
-    <PackageReference Include="TagLibSharp" Version="2.3.0" />
+    <PackageReference Include="TagLibSharp" />
   </ItemGroup>
   </ItemGroup>
 
 
   <PropertyGroup>
   <PropertyGroup>
@@ -26,13 +26,13 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
     </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>
   </ItemGroup>
 
 
 </Project>
 </Project>

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 using System;
 using System;
 using System.Collections.Concurrent;
 using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Collections.Generic;
@@ -33,15 +31,10 @@ namespace Emby.Server.Implementations.AppBase
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
 
 
-        /// <summary>
-        /// The _configuration loaded.
-        /// </summary>
-        private bool _configurationLoaded;
-
         /// <summary>
         /// <summary>
         /// The _configuration.
         /// The _configuration.
         /// </summary>
         /// </summary>
-        private BaseApplicationConfiguration _configuration;
+        private BaseApplicationConfiguration? _configuration;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
         /// Initializes a new instance of the <see cref="BaseConfigurationManager" /> class.
@@ -63,17 +56,17 @@ namespace Emby.Server.Implementations.AppBase
         /// <summary>
         /// <summary>
         /// Occurs when [configuration updated].
         /// Occurs when [configuration updated].
         /// </summary>
         /// </summary>
-        public event EventHandler<EventArgs> ConfigurationUpdated;
+        public event EventHandler<EventArgs>? ConfigurationUpdated;
 
 
         /// <summary>
         /// <summary>
         /// Occurs when [configuration updating].
         /// Occurs when [configuration updating].
         /// </summary>
         /// </summary>
-        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
+        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdating;
 
 
         /// <summary>
         /// <summary>
         /// Occurs when [named configuration updated].
         /// Occurs when [named configuration updated].
         /// </summary>
         /// </summary>
-        public event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
+        public event EventHandler<ConfigurationUpdateEventArgs>? NamedConfigurationUpdated;
 
 
         /// <summary>
         /// <summary>
         /// Gets the type of the configuration.
         /// Gets the type of the configuration.
@@ -107,31 +100,25 @@ namespace Emby.Server.Implementations.AppBase
         {
         {
             get
             get
             {
             {
-                if (_configurationLoaded)
+                if (_configuration is not null)
                 {
                 {
                     return _configuration;
                     return _configuration;
                 }
                 }
 
 
                 lock (_configurationSyncLock)
                 lock (_configurationSyncLock)
                 {
                 {
-                    if (_configurationLoaded)
+                    if (_configuration is not null)
                     {
                     {
                         return _configuration;
                         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
             protected set
             {
             {
                 _configuration = value;
                 _configuration = value;
-
-                _configurationLoaded = value is not null;
             }
             }
         }
         }
 
 
@@ -183,7 +170,7 @@ namespace Emby.Server.Implementations.AppBase
             Logger.LogInformation("Saving system configuration");
             Logger.LogInformation("Saving system configuration");
             var path = CommonApplicationPaths.SystemConfigurationFilePath;
             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)
             lock (_configurationSyncLock)
             {
             {
@@ -323,25 +310,20 @@ namespace Emby.Server.Implementations.AppBase
 
 
         private object LoadConfiguration(string path, Type configurationType)
         private object LoadConfiguration(string path, Type configurationType)
         {
         {
-            if (!File.Exists(path))
-            {
-                return Activator.CreateInstance(configurationType);
-            }
-
             try
             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);
                 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 />
         /// <inheritdoc />
@@ -367,7 +349,7 @@ namespace Emby.Server.Implementations.AppBase
             _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
             _configurations.AddOrUpdate(key, configuration, (_, _) => configuration);
 
 
             var path = GetConfigurationFile(key);
             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)
             lock (_configurationSyncLock)
             {
             {

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

@@ -11,7 +11,6 @@ using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Net;
 using System.Net;
 using System.Reflection;
 using System.Reflection;
-using System.Runtime.InteropServices;
 using System.Security.Cryptography.X509Certificates;
 using System.Security.Cryptography.X509Certificates;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
@@ -81,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.System;
 using MediaBrowser.Model.System;
@@ -113,15 +114,11 @@ namespace Emby.Server.Implementations
     /// </summary>
     /// </summary>
     public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
     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>
         /// <summary>
         /// The disposable parts.
         /// The disposable parts.
         /// </summary>
         /// </summary>
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
+        private readonly DeviceId _deviceId;
 
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IFileSystem _fileSystemManager;
         private readonly IConfiguration _startupConfig;
         private readonly IConfiguration _startupConfig;
@@ -130,7 +127,6 @@ namespace Emby.Server.Implementations
         private readonly IPluginManager _pluginManager;
         private readonly IPluginManager _pluginManager;
 
 
         private List<Type> _creatingInstances;
         private List<Type> _creatingInstances;
-        private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
         private ISessionManager _sessionManager;
 
 
         /// <summary>
         /// <summary>
@@ -139,8 +135,6 @@ namespace Emby.Server.Implementations
         /// <value>All concrete types.</value>
         /// <value>All concrete types.</value>
         private Type[] _allConcreteTypes;
         private Type[] _allConcreteTypes;
 
 
-        private DeviceId _deviceId;
-
         private bool _disposed = false;
         private bool _disposed = false;
 
 
         /// <summary>
         /// <summary>
@@ -164,6 +158,7 @@ namespace Emby.Server.Implementations
 
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
             _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
             _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
+            _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
 
 
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
             ApplicationVersionString = ApplicationVersion.ToString(3);
             ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -191,23 +186,9 @@ namespace Emby.Server.Implementations
 
 
         public bool CoreStartupHasCompleted { get; private set; }
         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>
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// Gets the <see cref="INetworkManager"/> singleton instance.
@@ -284,15 +265,7 @@ namespace Emby.Server.Implementations
         /// <value>The application name.</value>
         /// <value>The application name.</value>
         public string ApplicationProductName { get; } = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).ProductName;
         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/>
         /// <inheritdoc/>
         public string Name => ApplicationProductName;
         public string Name => ApplicationProductName;
@@ -445,7 +418,7 @@ namespace Emby.Server.Implementations
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
             ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated;
             ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
             ConfigurationManager.NamedConfigurationUpdated += OnConfigurationUpdated;
 
 
-            _mediaEncoder.SetFFmpegPath();
+            Resolve<IMediaEncoder>().SetFFmpegPath();
 
 
             Logger.LogInformation("ServerId: {ServerId}", SystemId);
             Logger.LogInformation("ServerId: {ServerId}", SystemId);
 
 
@@ -558,6 +531,8 @@ namespace Emby.Server.Implementations
 
 
             serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
             serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
 
+            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
             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>();
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
             await localizationManager.LoadAll().ConfigureAwait(false);
 
 
-            _mediaEncoder = Resolve<IMediaEncoder>();
             _sessionManager = Resolve<ISessionManager>();
             _sessionManager = Resolve<ISessionManager>();
 
 
             SetStaticProperties();
             SetStaticProperties();
 
 
-            var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
-            ((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
-
             FindParts();
             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)
         private X509Certificate2 GetCertificate(string path, string password)
         {
         {
             if (string.IsNullOrWhiteSpace(path))
             if (string.IsNullOrWhiteSpace(path))
@@ -782,10 +726,6 @@ namespace Emby.Server.Implementations
 
 
             Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
             Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>());
 
 
-            Resolve<ISubtitleManager>().AddParts(GetExports<ISubtitleProvider>());
-
-            Resolve<IChannelManager>().AddParts(GetExports<IChannel>());
-
             Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>());
             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="userDataManager">The user data manager.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="providerManager">The provider manager.</param>
         /// <param name="memoryCache">The memory cache.</param>
         /// <param name="memoryCache">The memory cache.</param>
+        /// <param name="channels">The channels.</param>
         public ChannelManager(
         public ChannelManager(
             IUserManager userManager,
             IUserManager userManager,
             IDtoService dtoService,
             IDtoService dtoService,
@@ -75,7 +76,8 @@ namespace Emby.Server.Implementations.Channels
             IFileSystem fileSystem,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
             IUserDataManager userDataManager,
             IProviderManager providerManager,
             IProviderManager providerManager,
-            IMemoryCache memoryCache)
+            IMemoryCache memoryCache,
+            IEnumerable<IChannel> channels)
         {
         {
             _userManager = userManager;
             _userManager = userManager;
             _dtoService = dtoService;
             _dtoService = dtoService;
@@ -86,18 +88,13 @@ namespace Emby.Server.Implementations.Channels
             _userDataManager = userDataManager;
             _userDataManager = userDataManager;
             _providerManager = providerManager;
             _providerManager = providerManager;
             _memoryCache = memoryCache;
             _memoryCache = memoryCache;
+            Channels = channels.ToArray();
         }
         }
 
 
-        internal IChannel[] Channels { get; private set; }
+        internal IChannel[] Channels { get; }
 
 
         private static TimeSpan CacheLength => TimeSpan.FromHours(3);
         private static TimeSpan CacheLength => TimeSpan.FromHours(3);
 
 
-        /// <inheritdoc />
-        public void AddParts(IEnumerable<IChannel> channels)
-        {
-            Channels = channels.ToArray();
-        }
-
         /// <inheritdoc />
         /// <inheritdoc />
         public bool EnableMediaSourceDisplay(BaseItem item)
         public bool EnableMediaSourceDisplay(BaseItem item)
         {
         {
@@ -160,16 +157,16 @@ namespace Emby.Server.Implementations.Channels
         }
         }
 
 
         /// <inheritdoc />
         /// <inheritdoc />
-        public QueryResult<Channel> GetChannelsInternal(ChannelQuery query)
+        public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query)
         {
         {
             var user = query.UserId.Equals(default)
             var user = query.UserId.Equals(default)
                 ? null
                 ? null
                 : _userManager.GetUserById(query.UserId);
                 : _userManager.GetUserById(query.UserId);
 
 
-            var channels = GetAllChannels()
-                .Select(GetChannelEntity)
+            var channels = await GetAllChannelEntitiesAsync()
                 .OrderBy(i => i.SortName)
                 .OrderBy(i => i.SortName)
-                .ToList();
+                .ToListAsync()
+                .ConfigureAwait(false);
 
 
             if (query.IsRecordingsFolder.HasValue)
             if (query.IsRecordingsFolder.HasValue)
             {
             {
@@ -229,6 +226,7 @@ namespace Emby.Server.Implementations.Channels
 
 
             if (user is not null)
             if (user is not null)
             {
             {
+                var userId = user.Id.ToString("N", CultureInfo.InvariantCulture);
                 channels = channels.Where(i =>
                 channels = channels.Where(i =>
                 {
                 {
                     if (!i.IsVisible(user))
                     if (!i.IsVisible(user))
@@ -238,7 +236,7 @@ namespace Emby.Server.Implementations.Channels
 
 
                     try
                     try
                     {
                     {
-                        return GetChannelProvider(i).IsEnabledFor(user.Id.ToString("N", CultureInfo.InvariantCulture));
+                        return GetChannelProvider(i).IsEnabledFor(userId);
                     }
                     }
                     catch
                     catch
                     {
                     {
@@ -261,7 +259,7 @@ namespace Emby.Server.Implementations.Channels
             {
             {
                 foreach (var item in all)
                 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 />
         /// <inheritdoc />
-        public QueryResult<BaseItemDto> GetChannels(ChannelQuery query)
+        public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query)
         {
         {
             var user = query.UserId.Equals(default)
             var user = query.UserId.Equals(default)
                 ? null
                 ? null
                 : _userManager.GetUserById(query.UserId);
                 : _userManager.GetUserById(query.UserId);
 
 
-            var internalResult = GetChannelsInternal(query);
+            var internalResult = await GetChannelsInternalAsync(query).ConfigureAwait(false);
 
 
             var dtoOptions = new DtoOptions();
             var dtoOptions = new DtoOptions();
 
 
@@ -330,9 +328,12 @@ namespace Emby.Server.Implementations.Channels
             progress.Report(100);
             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)
         private MediaSourceInfo[] GetSavedMediaSources(BaseItem item)
@@ -404,7 +405,7 @@ namespace Emby.Server.Implementations.Channels
             }
             }
             else
             else
             {
             {
-                results = new List<MediaSourceInfo>();
+                results = Enumerable.Empty<MediaSourceInfo>();
             }
             }
 
 
             return results
             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");
             return Path.Combine(_appPaths.DataPath, "collections");
         }
         }
 
 
-        private Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
+        /// <inheritdoc />
+        public Task<Folder?> GetCollectionsFolder(bool createIfNeeded)
         {
         {
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
             return EnsureLibraryFolder(GetCollectionsFolderPath(), createIfNeeded);
         }
         }
@@ -206,8 +207,7 @@ namespace Emby.Server.Implementations.Collections
                 throw new ArgumentException("No collection exists with the supplied Id");
                 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 linkedChildrenList = collection.GetLinkedChildren();
             var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
             var currentLinkedChildrenIds = linkedChildrenList.Select(i => i.Id).ToList();
@@ -223,18 +223,23 @@ namespace Emby.Server.Implementations.Collections
 
 
                 if (!currentLinkedChildrenIds.Contains(id))
                 if (!currentLinkedChildrenIds.Contains(id))
                 {
                 {
-                    itemList.Add(item);
+                    (itemList ??= new()).Add(item);
 
 
-                    list.Add(LinkedChild.Create(item));
                     linkedChildrenList.Add(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);
                 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.LinkedChildren = newChildren;
                 collection.UpdateRatingToItems(linkedChildrenList);
                 collection.UpdateRatingToItems(linkedChildrenList);
 
 

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

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

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

@@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// <summary>
         /// Gets a new copy of the default configuration options.
         /// Gets a new copy of the default configuration options.
         /// </summary>
         /// </summary>
-        public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
+        public static Dictionary<string, string?> DefaultConfiguration => new()
         {
         {
             { HostWebClientKey, bool.TrueString },
             { HostWebClientKey, bool.TrueString },
-            { DefaultRedirectKey, "web/index.html" },
+            { DefaultRedirectKey, "web/" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegProbeSizeKey, "1G" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { FfmpegAnalyzeDurationKey, "200M" },
             { PlaylistsAllowDuplicatesKey, bool.FalseString },
             { 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;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Threading;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 using SQLitePCL.pretty;
 using SQLitePCL.pretty;
@@ -27,9 +26,19 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Gets or sets the path to the DB file.
         /// Gets or sets the path to the DB file.
         /// </summary>
         /// </summary>
-        /// <value>Path to the DB file.</value>
         protected string DbFilePath { get; set; }
         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>
         /// <summary>
         /// Gets the logger.
         /// Gets the logger.
         /// </summary>
         /// </summary>
@@ -63,7 +72,7 @@ namespace Emby.Server.Implementations.Data
         /// <summary>
         /// <summary>
         /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
         /// Gets the locking mode. <see href="https://www.sqlite.org/pragma.html#pragma_locking_mode" />.
         /// </summary>
         /// </summary>
-        protected virtual string LockingMode => "EXCLUSIVE";
+        protected virtual string LockingMode => "NORMAL";
 
 
         /// <summary>
         /// <summary>
         /// Gets the journal mode. <see href="https://www.sqlite.org/pragma.html#pragma_journal_mode" />.
         /// 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>
         /// <summary>
         /// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
         /// 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>
         /// </summary>
         /// <value>The journal size limit.</value>
         /// <value>The journal size limit.</value>
-        protected virtual int? JournalSizeLimit => 0;
+        protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
 
 
         /// <summary>
         /// <summary>
         /// Gets the page size.
         /// Gets the page size.
@@ -88,7 +98,7 @@ namespace Emby.Server.Implementations.Data
         /// </summary>
         /// </summary>
         /// <value>The temp store mode.</value>
         /// <value>The temp store mode.</value>
         /// <see cref="TempStoreMode"/>
         /// <see cref="TempStoreMode"/>
-        protected virtual TempStoreMode TempStore => TempStoreMode.Default;
+        protected virtual TempStoreMode TempStore => TempStoreMode.Memory;
 
 
         /// <summary>
         /// <summary>
         /// Gets the synchronous mode.
         /// Gets the synchronous mode.
@@ -101,83 +111,114 @@ namespace Emby.Server.Implementations.Data
         /// Gets or sets the write lock.
         /// Gets or sets the write lock.
         /// </summary>
         /// </summary>
         /// <value>The write lock.</value>
         /// <value>The write lock.</value>
-        protected SemaphoreSlim WriteLock { get; set; } = new SemaphoreSlim(1, 1);
+        protected ConnectionPool WriteConnections { get; set; }
 
 
         /// <summary>
         /// <summary>
         /// Gets or sets the write connection.
         /// Gets or sets the write connection.
         /// </summary>
         /// </summary>
         /// <value>The write connection.</value>
         /// <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,
                 DbFilePath,
                 DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
                 DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
                 null);
                 null);
 
 
             if (CacheSize.HasValue)
             if (CacheSize.HasValue)
             {
             {
-                WriteConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
+                writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
             }
             }
 
 
             if (!string.IsNullOrWhiteSpace(LockingMode))
             if (!string.IsNullOrWhiteSpace(LockingMode))
             {
             {
-                WriteConnection.Execute("PRAGMA locking_mode=" + LockingMode);
+                writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
             }
             }
 
 
             if (!string.IsNullOrWhiteSpace(JournalMode))
             if (!string.IsNullOrWhiteSpace(JournalMode))
             {
             {
-                WriteConnection.Execute("PRAGMA journal_mode=" + JournalMode);
+                writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
             }
             }
 
 
             if (JournalSizeLimit.HasValue)
             if (JournalSizeLimit.HasValue)
             {
             {
-                WriteConnection.Execute("PRAGMA journal_size_limit=" + (int)JournalSizeLimit.Value);
+                writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
             }
             }
 
 
             if (Synchronous.HasValue)
             if (Synchronous.HasValue)
             {
             {
-                WriteConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
+                writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
             }
             }
 
 
             if (PageSize.HasValue)
             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)
         protected bool TableExists(ManagedConnection connection, string name)
         {
         {
             return connection.RunInTransaction(
             return connection.RunInTransaction(
@@ -252,22 +293,10 @@ namespace Emby.Server.Implementations.Data
 
 
             if (dispose)
             if (dispose)
             {
             {
-                WriteLock.Wait();
-                try
-                {
-                    WriteConnection?.Dispose();
-                }
-                finally
-                {
-                    WriteLock.Release();
-                }
-
-                WriteLock.Dispose();
+                WriteConnections.Dispose();
+                ReadConnections.Dispose();
             }
             }
 
 
-            WriteConnection = null;
-            WriteLock = null;
-
             _disposed = true;
             _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;
 using System.Collections.Generic;
 using System.Collections.Generic;
-using System.Threading;
 using SQLitePCL.pretty;
 using SQLitePCL.pretty;
 
 
 namespace Emby.Server.Implementations.Data
 namespace Emby.Server.Implementations.Data
 {
 {
     public sealed class ManagedConnection : IDisposable
     public sealed class ManagedConnection : IDisposable
     {
     {
-        private readonly SemaphoreSlim _writeLock;
+        private readonly ConnectionPool _pool;
 
 
-        private SQLiteDatabaseConnection? _db;
+        private SQLiteDatabaseConnection _db;
 
 
         private bool _disposed = false;
         private bool _disposed = false;
 
 
-        public ManagedConnection(SQLiteDatabaseConnection db, SemaphoreSlim writeLock)
+        public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
         {
         {
             _db = db;
             _db = db;
-            _writeLock = writeLock;
+            _pool = pool;
         }
         }
 
 
         public IStatement PrepareStatement(string sql)
         public IStatement PrepareStatement(string sql)
@@ -73,9 +72,9 @@ namespace Emby.Server.Implementations.Data
                 return;
                 return;
             }
             }
 
 
-            _writeLock.Release();
+            _pool.Return(_db);
 
 
-            _db = null; // Don't dispose it
+            _db = null!; // Don't dispose it
             _disposed = true;
             _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.IO;
 using System.Threading;
 using System.Threading;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
-using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Persistence;
@@ -18,33 +18,32 @@ namespace Emby.Server.Implementations.Data
 {
 {
     public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
     public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository
     {
     {
+        private readonly IUserManager _userManager;
+
         public SqliteUserDataRepository(
         public SqliteUserDataRepository(
             ILogger<SqliteUserDataRepository> logger,
             ILogger<SqliteUserDataRepository> logger,
-            IApplicationPaths appPaths)
+            IServerConfigurationManager config,
+            IUserManager userManager)
             : base(logger)
             : base(logger)
         {
         {
-            DbFilePath = Path.Combine(appPaths.DataPath, "library.db");
+            _userManager = userManager;
+
+            DbFilePath = Path.Combine(config.ApplicationPaths.DataPath, "library.db");
         }
         }
 
 
         /// <summary>
         /// <summary>
         /// Opens the connection to the database.
         /// Opens the connection to the database.
         /// </summary>
         /// </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())
             using (var connection = GetConnection())
             {
             {
                 var userDatasTableExists = TableExists(connection, "UserDatas");
                 var userDatasTableExists = TableExists(connection, "UserDatas");
                 var userDataTableExists = TableExists(connection, "userdata");
                 var userDataTableExists = TableExists(connection, "userdata");
 
 
-                var users = userDatasTableExists ? null : userManager.Users;
+                var users = userDatasTableExists ? null : _userManager.Users;
 
 
                 connection.RunInTransaction(
                 connection.RunInTransaction(
                     db =>
                     db =>
@@ -371,20 +370,5 @@ namespace Emby.Server.Implementations.Data
 
 
             return userData;
             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.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
-using Jellyfin.Api.Helpers;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
@@ -83,22 +82,23 @@ namespace Emby.Server.Implementations.Dto
         /// <inheritdoc />
         /// <inheritdoc />
         public IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User user = null, BaseItem owner = null)
         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);
                 var dto = GetBaseItemDtoInternal(item, options, user, owner);
 
 
                 if (item is LiveTvChannel tvChannel)
                 if (item is LiveTvChannel tvChannel)
                 {
                 {
-                    channelTuples.Add((dto, tvChannel));
+                    (channelTuples ??= new()).Add((dto, tvChannel));
                 }
                 }
                 else if (item is LiveTvProgram)
                 else if (item is LiveTvProgram)
                 {
                 {
-                    programTuples.Add((item, dto));
+                    (programTuples ??= new()).Add((item, dto));
                 }
                 }
 
 
                 if (item is IItemByName byName)
                 if (item is IItemByName byName)
@@ -121,12 +121,12 @@ namespace Emby.Server.Implementations.Dto
                 returnItems[index] = dto;
                 returnItems[index] = dto;
             }
             }
 
 
-            if (programTuples.Count > 0)
+            if (programTuples is not null)
             {
             {
                 LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
                 LivetvManager.AddInfoToProgramDto(programTuples, options.Fields, user).GetAwaiter().GetResult();
             }
             }
 
 
-            if (channelTuples.Count > 0)
+            if (channelTuples is not null)
             {
             {
                 LivetvManager.AddChannelInfo(channelTuples, options, user);
                 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)
             var people = _libraryManager.GetPeople(item).OrderBy(i => i.SortOrder ?? int.MaxValue)
                 .ThenBy(i =>
                 .ThenBy(i =>
                 {
                 {
-                    if (i.IsType(PersonType.Actor))
+                    if (i.IsType(PersonKind.Actor))
                     {
                     {
                         return 0;
                         return 0;
                     }
                     }
 
 
-                    if (i.IsType(PersonType.GuestStar))
+                    if (i.IsType(PersonKind.GuestStar))
                     {
                     {
                         return 1;
                         return 1;
                     }
                     }
 
 
-                    if (i.IsType(PersonType.Director))
+                    if (i.IsType(PersonKind.Director))
                     {
                     {
                         return 2;
                         return 2;
                     }
                     }
 
 
-                    if (i.IsType(PersonType.Writer))
+                    if (i.IsType(PersonKind.Writer))
                     {
                     {
                         return 3;
                         return 3;
                     }
                     }
 
 
-                    if (i.IsType(PersonType.Producer))
+                    if (i.IsType(PersonKind.Producer))
                     {
                     {
                         return 4;
                         return 4;
                     }
                     }
 
 
-                    if (i.IsType(PersonType.Composer))
+                    if (i.IsType(PersonKind.Composer))
                     {
                     {
                         return 4;
                         return 4;
                     }
                     }
@@ -571,9 +571,7 @@ namespace Emby.Server.Implementations.Dto
                         return null;
                         return null;
                     }
                     }
                 }).Where(i => i is not 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)
                 .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
                 .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
 
 
@@ -908,6 +906,7 @@ namespace Emby.Server.Implementations.Dto
             // Add audio info
             // Add audio info
             if (item is Audio audio)
             if (item is Audio audio)
             {
             {
+                dto.LUFS = audio.LUFS;
                 dto.Album = audio.Album;
                 dto.Album = audio.Album;
                 if (audio.ExtraType.HasValue)
                 if (audio.ExtraType.HasValue)
                 {
                 {

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

@@ -22,17 +22,17 @@
   </ItemGroup>
   </ItemGroup>
 
 
   <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>
 
 
   <ItemGroup>
   <ItemGroup>
@@ -53,13 +53,13 @@
 
 
   <!-- Code Analyzers-->
   <!-- Code Analyzers-->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
-    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4">
+    <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
     </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>
 
 
   <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.Entities;
 using Jellyfin.Data.Events;
 using Jellyfin.Data.Events;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Channels;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -26,12 +27,8 @@ namespace Emby.Server.Implementations.EntryPoints
 {
 {
     public class LibraryChangedNotifier : IServerEntryPoint
     public class LibraryChangedNotifier : IServerEntryPoint
     {
     {
-        /// <summary>
-        /// The library update duration.
-        /// </summary>
-        private const int LibraryUpdateDuration = 30000;
-
         private readonly ILibraryManager _libraryManager;
         private readonly ILibraryManager _libraryManager;
+        private readonly IServerConfigurationManager _configurationManager;
         private readonly IProviderManager _providerManager;
         private readonly IProviderManager _providerManager;
         private readonly ISessionManager _sessionManager;
         private readonly ISessionManager _sessionManager;
         private readonly IUserManager _userManager;
         private readonly IUserManager _userManager;
@@ -51,12 +48,14 @@ namespace Emby.Server.Implementations.EntryPoints
 
 
         public LibraryChangedNotifier(
         public LibraryChangedNotifier(
             ILibraryManager libraryManager,
             ILibraryManager libraryManager,
+            IServerConfigurationManager configurationManager,
             ISessionManager sessionManager,
             ISessionManager sessionManager,
             IUserManager userManager,
             IUserManager userManager,
             ILogger<LibraryChangedNotifier> logger,
             ILogger<LibraryChangedNotifier> logger,
             IProviderManager providerManager)
             IProviderManager providerManager)
         {
         {
             _libraryManager = libraryManager;
             _libraryManager = libraryManager;
+            _configurationManager = configurationManager;
             _sessionManager = sessionManager;
             _sessionManager = sessionManager;
             _userManager = userManager;
             _userManager = userManager;
             _logger = logger;
             _logger = logger;
@@ -196,12 +195,12 @@ namespace Emby.Server.Implementations.EntryPoints
                     LibraryUpdateTimer = new Timer(
                     LibraryUpdateTimer = new Timer(
                         LibraryUpdateTimerCallback,
                         LibraryUpdateTimerCallback,
                         null,
                         null,
-                        LibraryUpdateDuration,
-                        Timeout.Infinite);
+                        TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration),
+                        Timeout.InfiniteTimeSpan);
                 }
                 }
                 else
                 else
                 {
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
                 }
 
 
                 if (e.Item.GetParent() is Folder parent)
                 if (e.Item.GetParent() is Folder parent)
@@ -229,11 +228,11 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             {
                 if (LibraryUpdateTimer is null)
                 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
                 else
                 {
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
                 }
 
 
                 _itemsUpdated.Add(e.Item);
                 _itemsUpdated.Add(e.Item);
@@ -256,11 +255,11 @@ namespace Emby.Server.Implementations.EntryPoints
             {
             {
                 if (LibraryUpdateTimer is null)
                 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
                 else
                 {
                 {
-                    LibraryUpdateTimer.Change(LibraryUpdateDuration, Timeout.Infinite);
+                    LibraryUpdateTimer.Change(TimeSpan.FromSeconds(_configurationManager.Configuration.LibraryUpdateDuration), Timeout.InfiniteTimeSpan);
                 }
                 }
 
 
                 if (e.Parent is Folder parent)
                 if (e.Parent is Folder parent)
@@ -276,25 +275,31 @@ namespace Emby.Server.Implementations.EntryPoints
         /// Libraries the update timer callback.
         /// Libraries the update timer callback.
         /// </summary>
         /// </summary>
         /// <param name="state">The state.</param>
         /// <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)
             lock (_libraryChangedSyncLock)
             {
             {
                 // Remove dupes in case some were saved multiple times
                 // Remove dupes in case some were saved multiple times
-                var foldersAddedTo = _foldersAddedTo
+                foldersAddedTo = _foldersAddedTo
                                         .DistinctBy(x => x.Id)
                                         .DistinctBy(x => x.Id)
                                         .ToList();
                                         .ToList();
 
 
-                var foldersRemovedFrom = _foldersRemovedFrom
+                foldersRemovedFrom = _foldersRemovedFrom
                                             .DistinctBy(x => x.Id)
                                             .DistinctBy(x => x.Id)
                                             .ToList();
                                             .ToList();
 
 
-                var itemsUpdated = _itemsUpdated
+                itemsUpdated = _itemsUpdated
                                     .Where(i => !_itemsAdded.Contains(i))
                                     .Where(i => !_itemsAdded.Contains(i))
                                     .DistinctBy(x => x.Id)
                                     .DistinctBy(x => x.Id)
                                     .ToList();
                                     .ToList();
 
 
-                SendChangeNotifications(_itemsAdded.ToList(), itemsUpdated, _itemsRemoved.ToList(), foldersAddedTo, foldersRemovedFrom, CancellationToken.None).GetAwaiter().GetResult();
+                itemsAdded = _itemsAdded.ToList();
+                itemsRemoved = _itemsRemoved.ToList();
 
 
                 if (LibraryUpdateTimer is not null)
                 if (LibraryUpdateTimer is not null)
                 {
                 {
@@ -308,6 +313,8 @@ namespace Emby.Server.Implementations.EntryPoints
                 _foldersAddedTo.Clear();
                 _foldersAddedTo.Clear();
                 _foldersRemovedFrom.Clear();
                 _foldersRemovedFrom.Clear();
             }
             }
+
+            await SendChangeNotifications(itemsAdded, itemsUpdated, itemsRemoved, foldersAddedTo, foldersRemovedFrom, CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 
         /// <summary>
         /// <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)
             lock (_syncLock)
             {
             {
                 // Remove dupes in case some were saved multiple times
                 // Remove dupes in case some were saved multiple times
-                var changes = _changedItems.ToList();
+                changes = _changedItems.ToList();
                 _changedItems.Clear();
                 _changedItems.Clear();
 
 
-                SendNotifications(changes, CancellationToken.None).GetAwaiter().GetResult();
-
                 if (_updateTimer is not null)
                 if (_updateTimer is not null)
                 {
                 {
                     _updateTimer.Dispose();
                     _updateTimer.Dispose();
                     _updateTimer = null;
                     _updateTimer = null;
                 }
                 }
             }
             }
+
+            await SendNotifications(changes, CancellationToken.None).ConfigureAwait(false);
         }
         }
 
 
         private async Task SendNotifications(List<KeyValuePair<Guid, List<BaseItem>>> changes, CancellationToken cancellationToken)
         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 System.Threading.Tasks;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Model.Session;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -85,6 +85,18 @@ namespace Emby.Server.Implementations.HttpServer
         /// <value>The state.</value>
         /// <value>The state.</value>
         public WebSocketState State => _socket.State;
         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>
         /// <summary>
         /// Sends a message asynchronously.
         /// Sends a message asynchronously.
         /// </summary>
         /// </summary>
@@ -224,7 +236,7 @@ namespace Emby.Server.Implementations.HttpServer
         {
         {
             LastKeepAliveDate = DateTime.UtcNow;
             LastKeepAliveDate = DateTime.UtcNow;
             return SendAsync(
             return SendAsync(
-                new WebSocketMessage<string>
+                new OutboundWebSocketMessage
                 {
                 {
                     MessageId = Guid.NewGuid(),
                     MessageId = Guid.NewGuid(),
                     MessageType = SessionMessageType.KeepAlive
                     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;
             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>
         /// <summary>
         /// Takes a filename and removes invalid characters.
         /// Takes a filename and removes invalid characters.
         /// </summary>
         /// </summary>
@@ -405,19 +386,18 @@ namespace Emby.Server.Implementations.IO
                 return;
                 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)
                 if (isHidden)
                 {
                 {
-                    File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
+                    File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
                 }
                 }
                 else
                 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;
                 return;
             }
             }
 
 
-            var info = GetExtendedFileSystemInfo(path);
+            var info = new FileInfo(path);
 
 
             if (!info.Exists)
             if (!info.Exists)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
+            if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
+                && ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
             {
             {
                 return;
                 return;
             }
             }
 
 
-            var attributes = File.GetAttributes(path);
+            var attributes = info.Attributes;
 
 
             if (readOnly)
             if (readOnly)
             {
             {
@@ -450,7 +431,7 @@ namespace Emby.Server.Implementations.IO
             }
             }
             else
             else
             {
             {
-                attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
+                attributes &= ~FileAttributes.ReadOnly;
             }
             }
 
 
             if (isHidden)
             if (isHidden)
@@ -459,17 +440,12 @@ namespace Emby.Server.Implementations.IO
             }
             }
             else
             else
             {
             {
-                attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
+                attributes &= ~FileAttributes.Hidden;
             }
             }
 
 
             File.SetAttributes(path, attributes);
             File.SetAttributes(path, attributes);
         }
         }
 
 
-        private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
-        {
-            return attributes & ~attributesToRemove;
-        }
-
         /// <summary>
         /// <summary>
         /// Swaps the files.
         /// Swaps the files.
         /// </summary>
         /// </summary>

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

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

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

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

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

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 #pragma warning disable CS1591
 
 
 using System.Collections.Generic;
 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="imageProcessor">The image processor.</param>
         /// <param name="memoryCache">The memory cache.</param>
         /// <param name="memoryCache">The memory cache.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="namingOptions">The naming options.</param>
+        /// <param name="directoryService">The directory service.</param>
         public LibraryManager(
         public LibraryManager(
             IServerApplicationHost appHost,
             IServerApplicationHost appHost,
             ILoggerFactory loggerFactory,
             ILoggerFactory loggerFactory,
@@ -128,7 +129,8 @@ namespace Emby.Server.Implementations.Library
             IItemRepository itemRepository,
             IItemRepository itemRepository,
             IImageProcessor imageProcessor,
             IImageProcessor imageProcessor,
             IMemoryCache memoryCache,
             IMemoryCache memoryCache,
-            NamingOptions namingOptions)
+            NamingOptions namingOptions,
+            IDirectoryService directoryService)
         {
         {
             _appHost = appHost;
             _appHost = appHost;
             _logger = loggerFactory.CreateLogger<LibraryManager>();
             _logger = loggerFactory.CreateLogger<LibraryManager>();
@@ -146,7 +148,7 @@ namespace Emby.Server.Implementations.Library
             _memoryCache = memoryCache;
             _memoryCache = memoryCache;
             _namingOptions = namingOptions;
             _namingOptions = namingOptions;
 
 
-            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions);
+            _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
 
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
 
@@ -356,8 +358,8 @@ namespace Emby.Server.Implementations.Library
             }
             }
 
 
             var children = item.IsFolder
             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))
             foreach (var metadataPath in GetMetadataPaths(item, children))
             {
             {
@@ -537,7 +539,7 @@ namespace Emby.Server.Implementations.Library
                 collectionType = GetContentTypeOverride(fullPath, true);
                 collectionType = GetContentTypeOverride(fullPath, true);
             }
             }
 
 
-            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, directoryService)
+            var args = new ItemResolveArgs(_configurationManager.ApplicationPaths, this)
             {
             {
                 Parent = parent,
                 Parent = parent,
                 FileInfo = fileInfo,
                 FileInfo = fileInfo,
@@ -1253,7 +1255,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 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);
                 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)
         public List<BaseItem> GetItemList(InternalItemsQuery query)
@@ -1277,7 +1286,7 @@ namespace Emby.Server.Implementations.Library
                 var parent = GetItemById(query.ParentId);
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 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);
                 var parent = GetItemById(query.ParentId);
                 if (parent is not null)
                 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));
                 _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))
             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();
                 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");
                 _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)
                 catch (Exception ex)
                 {
                 {
                     _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
                     _logger.LogError(ex, "Cannot get image dimensions for {ImagePath}", image.Path);
-                    size = new ImageDimensions(0, 0);
+                    size = default;
                     image.Width = 0;
                     image.Width = 0;
                     image.Height = 0;
                     image.Height = 0;
                 }
                 }
@@ -2741,9 +2756,7 @@ namespace Emby.Server.Implementations.Library
                 }
                 }
             })
             })
             .Where(i => i is not null)
             .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();
             .ToList();
         }
         }
 
 
@@ -2876,7 +2889,7 @@ namespace Emby.Server.Implementations.Library
 
 
         private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
         private async Task SavePeopleMetadataAsync(IEnumerable<PersonInfo> people, CancellationToken cancellationToken)
         {
         {
-            var personsToSave = new List<BaseItem>();
+            List<BaseItem> personsToSave = null;
 
 
             foreach (var person in people)
             foreach (var person in people)
             {
             {
@@ -2918,12 +2931,12 @@ namespace Emby.Server.Implementations.Library
 
 
                 if (saveEntity)
                 if (saveEntity)
                 {
                 {
-                    personsToSave.Add(personEntity);
+                    (personsToSave ??= new()).Add(personEntity);
                     await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
                     await RunMetadataSavers(personEntity, itemUpdateType).ConfigureAwait(false);
                 }
                 }
             }
             }
 
 
-            if (personsToSave.Count > 0)
+            if (personsToSave is not null)
             {
             {
                 CreateItems(personsToSave, null, CancellationToken.None);
                 CreateItems(personsToSave, null, CancellationToken.None);
             }
             }
@@ -3085,22 +3098,19 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentNullException(nameof(path));
                 throw new ArgumentNullException(nameof(path));
             }
             }
 
 
-            var removeList = new List<NameValuePair>();
+            List<NameValuePair> removeList = null;
 
 
             foreach (var contentType in _configurationManager.Configuration.ContentTypes)
             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))
                     || _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
                 _configurationManager.Configuration.ContentTypes = _configurationManager.Configuration.ContentTypes
                     .Except(removeList)
                     .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 file is strm or main media stream is missing, force a metadata refresh with remote probing
             if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
             if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder
                 && (item.Path.EndsWith(".strm", StringComparison.OrdinalIgnoreCase)
                 && (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(
                 await item.RefreshMetadata(
                     new MetadataRefreshOptions(_directoryService)
                     new MetadataRefreshOptions(_directoryService)

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

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
+using System.IO;
 using MediaBrowser.Common.Providers;
 using MediaBrowser.Common.Providers;
 
 
 namespace Emby.Server.Implementations.Library
 namespace Emby.Server.Implementations.Library
@@ -86,24 +87,8 @@ namespace Emby.Server.Implementations.Library
                 return false;
                 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
             // 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
             // when the sub path matches a similar but in-complete subpath
@@ -127,5 +112,82 @@ namespace Emby.Server.Implementations.Library
 
 
             return true;
             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)
         private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
         {
         {
             var files = new List<FileSystemMetadata>();
             var files = new List<FileSystemMetadata>();
-            var items = new List<BaseItem>();
             var leftOver = new List<FileSystemMetadata>();
             var leftOver = new List<FileSystemMetadata>();
 
 
             // Loop through each child file/folder and see if we find a video
             // 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
             var result = new MultiItemResolverResult
             {
             {
                 ExtraFiles = leftOver,
                 ExtraFiles = leftOver,
-                Items = items
+                Items = new List<BaseItem>()
             };
             };
 
 
             var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
             var isInMixedFolder = resolverResult.Count > 1 || (parent is not null && parent.IsTopParent);
@@ -193,7 +192,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                     continue;
                     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;
                     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 ILogger<MusicAlbumResolver> _logger;
         private readonly NamingOptions _namingOptions;
         private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
         /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</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;
             _logger = logger;
             _namingOptions = namingOptions;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -109,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 }
                 }
 
 
                 // If args contains music it's a music album
                 // If args contains music it's a music album
-                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
+                if (ContainsMusic(args.FileSystemChildren, true, _directoryService))
                 {
                 {
                     return true;
                     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 Emby.Naming.Common;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -18,19 +19,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
     {
         private readonly ILogger<MusicAlbumResolver> _logger;
         private readonly ILogger<MusicAlbumResolver> _logger;
-        private NamingOptions _namingOptions;
+        private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
 
         /// <summary>
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
         /// </summary>
         /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
         /// <param name="logger">Instance of the <see cref="MusicAlbumResolver"/> interface.</param>
         /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
         /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param>
+        /// <param name="directoryService">The directory service.</param>
         public MusicArtistResolver(
         public MusicArtistResolver(
             ILogger<MusicAlbumResolver> logger,
             ILogger<MusicAlbumResolver> logger,
-            NamingOptions namingOptions)
+            NamingOptions namingOptions,
+            IDirectoryService directoryService)
         {
         {
             _logger = logger;
             _logger = logger;
             _namingOptions = namingOptions;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -78,9 +83,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 return null;
                 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);
             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 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
                     // Stop once we see a music album
                     state.Stop();
                     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;
         private readonly ILogger _logger;
 
 
-        protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions)
+        protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
         {
         {
             _logger = logger;
             _logger = logger;
             NamingOptions = namingOptions;
             NamingOptions = namingOptions;
+            DirectoryService = directoryService;
         }
         }
 
 
         protected NamingOptions NamingOptions { get; }
         protected NamingOptions NamingOptions { get; }
 
 
+        protected IDirectoryService DirectoryService { get; }
+
         /// <summary>
         /// <summary>
         /// Resolves the specified args.
         /// Resolves the specified args.
         /// </summary>
         /// </summary>
@@ -65,13 +68,26 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     var filename = child.Name;
                     var filename = child.Name;
                     if (child.IsDirectory)
                     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))
                     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.Common;
 using Emby.Naming.Video;
 using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
@@ -14,7 +16,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// <summary>
     /// <summary>
     /// Resolves a Path into a Video or Video subclass.
     /// Resolves a Path into a Video or Video subclass.
     /// </summary>
     /// </summary>
-    internal class ExtraResolver
+    internal class ExtraResolver : BaseVideoResolver<Video>
     {
     {
         private readonly NamingOptions _namingOptions;
         private readonly NamingOptions _namingOptions;
         private readonly IItemResolver[] _trailerResolvers;
         private readonly IItemResolver[] _trailerResolvers;
@@ -25,11 +27,18 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</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;
             _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>
         /// <summary>

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

@@ -2,6 +2,7 @@
 
 
 using Emby.Naming.Common;
 using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
 namespace Emby.Server.Implementations.Library.Resolvers
 namespace Emby.Server.Implementations.Library.Resolvers
@@ -18,8 +19,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</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="imageProcessor">The image processor.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</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;
             _imageProcessor = imageProcessor;
         }
         }
@@ -97,12 +98,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
 
                 if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
                 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))
                 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))
                 if (string.IsNullOrEmpty(collectionType))
@@ -118,12 +119,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                         return null;
                         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))
                 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
                 // ignore extras
@@ -313,13 +314,8 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return result;
             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)
         private static bool ContainsFile(IReadOnlyList<VideoInfo> result, FileSystemMetadata file)
         {
         {

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

@@ -1,7 +1,5 @@
 #nullable disable
 #nullable disable
 
 
-#pragma warning disable CS1591
-
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.IO;
 using System.IO;
@@ -12,15 +10,20 @@ using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 
 
 namespace Emby.Server.Implementations.Library.Resolvers
 namespace Emby.Server.Implementations.Library.Resolvers
 {
 {
+    /// <summary>
+    /// Class PhotoResolver.
+    /// </summary>
     public class PhotoResolver : ItemResolver<Photo>
     public class PhotoResolver : ItemResolver<Photo>
     {
     {
         private readonly IImageProcessor _imageProcessor;
         private readonly IImageProcessor _imageProcessor;
         private readonly NamingOptions _namingOptions;
         private readonly NamingOptions _namingOptions;
+        private readonly IDirectoryService _directoryService;
 
 
         private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
         {
@@ -35,10 +38,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
             "default"
             "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;
             _imageProcessor = imageProcessor;
             _namingOptions = namingOptions;
             _namingOptions = namingOptions;
+            _directoryService = directoryService;
         }
         }
 
 
         /// <summary>
         /// <summary>
@@ -61,7 +71,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                         var filename = Path.GetFileNameWithoutExtension(args.Path);
                         var filename = Path.GetFileNameWithoutExtension(args.Path);
 
 
                         // Make sure the image doesn't belong to a video file
                         // 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)
                         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)
             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));
                 var filename = Path.GetFileName(Path.TrimEndingDirectorySeparator(args.Path));
                 if (string.IsNullOrEmpty(filename))
                 if (string.IsNullOrEmpty(filename))
                 {
                 {
@@ -42,7 +42,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return new Playlist
                     return new Playlist
                     {
                     {
                         Path = args.Path,
                         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
                     return new Playlist
                     {
                     {
                         Path = args.Path,
                         Path = args.Path,
-                        Name = filename
+                        Name = filename,
+                        OpenAccess = true
                     };
                     };
                 }
                 }
             }
             }
@@ -70,7 +72,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                         Path = args.Path,
                         Path = args.Path,
                         Name = Path.GetFileNameWithoutExtension(args.Path),
                         Name = Path.GetFileNameWithoutExtension(args.Path),
                         IsInMixedFolder = true,
                         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 Emby.Naming.Common;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging;
 
 
@@ -20,8 +21,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// </summary>
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="logger">The logger.</param>
         /// <param name="namingOptions">The naming options.</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)
                 if (season.IndexNumber.HasValue)
                 {
                 {
                     var seasonNumber = season.IndexNumber.Value;
                     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;
                 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 justName = Path.GetFileName(path.AsSpan());
 
 
+            var imdbId = justName.GetAttributeValue("imdbid");
+            if (!string.IsNullOrEmpty(imdbId))
+            {
+                item.SetProviderId(MetadataProvider.Imdb, imdbId);
+            }
+
             var tvdbId = justName.GetAttributeValue("tvdbid");
             var tvdbId = justName.GetAttributeValue("tvdbid");
             if (!string.IsNullOrEmpty(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)
         public Folder[] GetUserViews(UserViewQuery query)
         {
         {
             var user = _userManager.GetUserById(query.UserId);
             var user = _userManager.GetUserById(query.UserId);
-
             if (user is null)
             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()
             var folders = _libraryManager.GetUserRootFolder()
@@ -58,7 +57,6 @@ namespace Emby.Server.Implementations.Library
                 .ToList();
                 .ToList();
 
 
             var groupedFolders = new List<ICollectionFolder>();
             var groupedFolders = new List<ICollectionFolder>();
-
             var list = new List<Folder>();
             var list = new List<Folder>();
 
 
             foreach (var folder in folders)
             foreach (var folder in folders)
@@ -66,6 +64,20 @@ namespace Emby.Server.Implementations.Library
                 var collectionFolder = folder as ICollectionFolder;
                 var collectionFolder = folder as ICollectionFolder;
                 var folderViewType = collectionFolder?.CollectionType;
                 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))
                 if (UserView.IsUserSpecific(folder))
                 {
                 {
                     list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
                     list.Add(_libraryManager.GetNamedView(user, folder.Name, folder.Id, folderViewType, null));
@@ -111,10 +123,10 @@ namespace Emby.Server.Implementations.Library
 
 
             if (query.IncludeExternalContent)
             if (query.IncludeExternalContent)
             {
             {
-                var channelResult = _channelManager.GetChannelsInternal(new ChannelQuery
+                var channelResult = _channelManager.GetChannelsInternalAsync(new ChannelQuery
                 {
                 {
                     UserId = query.UserId
                     UserId = query.UserId
-                });
+                }).GetAwaiter().GetResult();
 
 
                 var channels = channelResult.Items;
                 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 sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
-
             var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
             var orders = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews);
 
 
             return list
             return list
                 .OrderBy(i =>
                 .OrderBy(i =>
                 {
                 {
                     var index = Array.IndexOf(orders, i.Id);
                     var index = Array.IndexOf(orders, i.Id);
-
                     if (index == -1
                     if (index == -1
                         && i is UserView view
                         && i is UserView view
                         && !view.DisplayParentId.Equals(default))
                         && !view.DisplayParentId.Equals(default))
@@ -286,7 +296,7 @@ namespace Emby.Server.Implementations.Library
 
 
             if (parents.Count == 0)
             if (parents.Count == 0)
             {
             {
-                return new List<BaseItem>();
+                return Array.Empty<BaseItem>();
             }
             }
 
 
             if (includeItemTypes.Length == 0)
             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);
                     _timerProvider.Update(existingTimer);
                     return Task.FromResult(existingTimer.Id);
                     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);
             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.WriteStartDocumentAsync(true).ConfigureAwait(false);
                     await writer.WriteStartElementAsync(null, "tvshow", null).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);
                         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 people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item);
 
 
                     var directors = people
                     var directors = people
-                        .Where(i => IsPersonType(i, PersonType.Director))
+                        .Where(i => i.IsType(PersonKind.Director))
                         .Select(i => i.Name)
                         .Select(i => i.Name)
                         .ToList();
                         .ToList();
 
 
@@ -2042,7 +2039,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                     }
                     }
 
 
                     var writers = people
                     var writers = people
-                        .Where(i => IsPersonType(i, PersonType.Writer))
+                        .Where(i => i.IsType(PersonKind.Writer))
                         .Select(i => i.Name)
                         .Select(i => i.Name)
                         .Distinct(StringComparer.OrdinalIgnoreCase)
                         .Distinct(StringComparer.OrdinalIgnoreCase)
                         .ToList();
                         .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)
         private LiveTvProgram GetProgramInfoFromCache(string programId)
         {
         {
             var query = new InternalItemsQuery
             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;
                 return null;
             }
             }
-            else if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
+
+            if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1)
             {
             {
                 return uri;
                 return uri;
             }
             }
-            else
-            {
-                return apiUrl + "/image/" + uri + "?token=" + token;
-            }
+
+            return apiUrl + "/image/" + uri + "?token=" + token;
         }
         }
 
 
         private static double GetAspectRatio(ImageDataDto i)
         private static double GetAspectRatio(ImageDataDto i)
@@ -463,10 +462,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             }
             }
 
 
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
             StringBuilder str = new StringBuilder("[", 1 + (programIds.Count * 13));
-            foreach (ReadOnlySpan<char> i in programIds)
+            foreach (var i in programIds)
             {
             {
                 str.Append('"')
                 str.Append('"')
-                    .Append(i.Slice(0, 10))
+                    .Append(i[..10])
                     .Append("\",");
                     .Append("\",");
             }
             }
 
 
@@ -570,15 +569,13 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                 _tokens.TryAdd(username, savedToken);
                 _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)
         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
             var programInfo = new ProgramInfo
             {
             {
                 ChannelId = program.ChannelId,
                 ChannelId = program.ChannelId,
                 EndDate = program.EndDate.UtcDateTime,
                 EndDate = program.EndDate.UtcDateTime,
-                EpisodeNumber = program.Episode?.Episode,
+                EpisodeNumber = program.Episode.Episode,
                 EpisodeTitle = episodeTitle,
                 EpisodeTitle = episodeTitle,
-                Genres = program.Categories,
+                Genres = programCategories,
                 StartDate = program.StartDate.UtcDateTime,
                 StartDate = program.StartDate.UtcDateTime,
                 Name = program.Title,
                 Name = program.Title,
                 Overview = program.Description,
                 Overview = program.Description,
                 ProductionYear = program.CopyrightDate?.Year,
                 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,
                 IsRepeat = program.IsPreviouslyShown && !program.IsNew,
                 IsPremiere = program.Premiere is not null,
                 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,
                 ImageUrl = string.IsNullOrEmpty(program.Icon?.Source) ? null : program.Icon.Source,
                 HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
                 HasImage = !string.IsNullOrEmpty(program.Icon?.Source),
                 OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
                 OfficialRating = string.IsNullOrEmpty(program.Rating?.Value) ? null : program.Rating.Value,
                 CommunityRating = program.StarRating,
                 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))
             if (string.IsNullOrWhiteSpace(program.ProgramId))
@@ -243,7 +244,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             {
             {
                 Id = c.Id,
                 Id = c.Id,
                 Name = c.DisplayName,
                 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
                 Number = string.IsNullOrWhiteSpace(c.Number) ? c.Id : c.Number
             }).ToList();
             }).ToList();
         }
         }

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

@@ -1312,20 +1312,19 @@ namespace Emby.Server.Implementations.LiveTv
             return 7;
             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)
             if (user is null)
             {
             {
                 return new QueryResult<BaseItem>();
                 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>();
             var excludeItemTypes = new List<BaseItemKind>();
 
 
-            if (folderIds.Count == 0)
+            if (folderIds.Length == 0)
             {
             {
                 return new QueryResult<BaseItem>();
                 return new QueryResult<BaseItem>();
             }
             }
@@ -1392,7 +1391,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
             {
                 MediaTypes = new[] { MediaType.Video },
                 MediaTypes = new[] { MediaType.Video },
                 Recursive = true,
                 Recursive = true,
-                AncestorIds = folderIds.ToArray(),
+                AncestorIds = folderIds,
                 IsFolder = false,
                 IsFolder = false,
                 IsVirtualItem = false,
                 IsVirtualItem = false,
                 Limit = limit,
                 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)
             var user = query.UserId.Equals(default)
                 ? null
                 ? null
@@ -1536,7 +1535,7 @@ namespace Emby.Server.Implementations.LiveTv
 
 
             RemoveFields(options);
             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);
             var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user);
 
 
@@ -2379,12 +2378,11 @@ namespace Emby.Server.Implementations.LiveTv
             return _tvDtoService.GetInternalProgramId(externalId);
             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()
             var folders = EmbyTV.EmbyTV.Current.GetRecordingFolders()
                 .SelectMany(i => i.Locations)
                 .SelectMany(i => i.Locations)
@@ -2396,14 +2394,16 @@ namespace Emby.Server.Implementations.LiveTv
                 .OrderBy(i => i.SortName)
                 .OrderBy(i => i.SortName)
                 .ToList();
                 .ToList();
 
 
-            folders.AddRange(_channelManager.GetChannelsInternal(new MediaBrowser.Model.Channels.ChannelQuery
+            var channels = await _channelManager.GetChannelsInternalAsync(new MediaBrowser.Model.Channels.ChannelQuery
             {
             {
                 UserId = user.Id,
                 UserId = user.Id,
                 IsRecordingsFolder = true,
                 IsRecordingsFolder = true,
                 RefreshLatestChannelItems = refreshChannels
                 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 System.Threading.Tasks;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using Jellyfin.Extensions.Json;
+using Jellyfin.Extensions.Json.Converters;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller;
@@ -58,7 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             _socketFactory = socketFactory;
             _socketFactory = socketFactory;
             _streamHelper = streamHelper;
             _streamHelper = streamHelper;
 
 
-            _jsonOptions = JsonDefaults.Options;
+            _jsonOptions = new JsonSerializerOptions(JsonDefaults.Options);
+            _jsonOptions.Converters.Add(new JsonBoolNumberConverter());
         }
         }
 
 
         public string Name => "HD Homerun";
         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)
         public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
         {
         {
             using var client = new TcpClient();
             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();
             using var stream = client.GetStream();
             return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
             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)
         public LegacyHdHomerunChannelCommands(string url)
         {
         {
             // parse url for channel and program
             // 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)
             if (match.Success)
             {
             {
                 _channel = match.Groups[1].Value;
                 _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
     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;
         private readonly IHttpClientFactory _httpClientFactory;
@@ -118,9 +120,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
             if (mediaSource.Protocol == MediaProtocol.Http && !mediaSource.RequiresLooping)
             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);
                     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);
             var attributes = ParseExtInf(extInf, out string remaining);
             extInf = 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))
             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;
             var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty;
 
 
             string numberString = null;
             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 (!IsValidChannelNumber(numberString))
             {
             {
                 if (attributes.TryGetValue("tvg-id", out attributeValue))
                 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;
                         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[] { ' ', '.' });
                             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();
                                 numberString = numberPart.ToString();
                             }
                             }
@@ -255,19 +254,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
 
         private static bool IsValidChannelNumber(string numberString)
         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 false;
             }
             }
 
 
-            return true;
+            return double.TryParse(numberString, CultureInfo.InvariantCulture, out _);
         }
         }
 
 
         private static string GetChannelName(string extInf, Dictionary<string, string> attributes)
         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[] { ' ', '.' });
                     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();
                         // channel.Number = number.ToString();
                         nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' });
                         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 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;
             remaining = line;
 
 

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

@@ -38,7 +38,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             _httpClientFactory = httpClientFactory;
             _httpClientFactory = httpClientFactory;
             _appHost = appHost;
             _appHost = appHost;
             OriginalStreamId = originalStreamId;
             OriginalStreamId = originalStreamId;
-            EnableStreamSharing = true;
         }
         }
 
 
         public override async Task Open(CancellationToken openCancellationToken)
         public override async Task Open(CancellationToken openCancellationToken)
@@ -59,39 +58,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
                 .GetAsync(url, HttpCompletionOption.ResponseHeadersRead, CancellationToken.None)
                 .ConfigureAwait(false);
                 .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);
             var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
 
 
             _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token);
             _ = 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.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
             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);
             var res = await taskCompletionSource.Task.ConfigureAwait(false);
             if (!res)
             if (!res)
             {
             {
@@ -108,15 +81,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                     try
                     try
                     {
                     {
                         Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
                         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)
                     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}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOnlineWithName": "{0}-এর সাথে সংযুক্ত হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
     "DeviceOfflineWithName": "{0}-এর সাথে সংযোগ বিচ্ছিন্ন হয়েছে",
-    "Collections": "সংগ্রহ",
+    "Collections": "সংগ্রহশালা",
     "ChapterNameValue": "অধ্যায় {0}",
     "ChapterNameValue": "অধ্যায় {0}",
-    "Channels": "চ্যানেল",
+    "Channels": "চ্যানেলসমূহ",
     "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
     "CameraImageUploadedFrom": "{0} থেকে একটি নতুন ক্যামেরার চিত্র আপলোড করা হয়েছে",
-    "Books": "বই",
+    "Books": "পুস্তকসমূহ",
     "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
     "AuthenticationSucceededWithUserName": "{0} অনুমোদন সফল",
-    "Artists": "শিল্পীরা",
+    "Artists": "শিল্পীগণ",
     "Application": "অ্যাপ্লিকেশন",
     "Application": "অ্যাপ্লিকেশন",
-    "Albums": "অ্যালবামগুলো",
+    "Albums": "অ্যালবামসমূহ",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
     "HeaderFavoriteEpisodes": "প্রিব পর্বগুলো",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteArtists": "প্রিয় শিল্পীরা",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderFavoriteAlbums": "প্রিয় এলবামগুলো",
     "HeaderContinueWatching": "দেখতে থাকুন",
     "HeaderContinueWatching": "দেখতে থাকুন",
-    "HeaderAlbumArtists": "লবাম শিল্পীবৃন্দ",
-    "Genres": "শৈলী",
-    "Folders": "ফোল্ডারগুলো",
+    "HeaderAlbumArtists": "অ্যালবাম শিল্পীবৃন্দ",
+    "Genres": "শৈলীধারাসমূহ",
+    "Folders": "ফোল্ডারসমূহ",
     "Favorites": "পছন্দসমূহ",
     "Favorites": "পছন্দসমূহ",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
     "FailedLoginAttemptWithUserName": "{0} লগিন করতে ব্যর্থ হয়েছে",
     "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
     "AppDeviceValues": "অ্যাপ: {0}, ডিভাইস: {0}",
     "VersionNumber": "সংস্করণ {0}",
     "VersionNumber": "সংস্করণ {0}",
-    "ValueSpecialEpisodeName": "বিশেষ - {0}",
+    "ValueSpecialEpisodeName": "বিশেষ পর্ব - {0}",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
     "ValueHasBeenAddedToLibrary": "আপনার লাইব্রেরিতে {0} যোগ করা হয়েছে",
     "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
     "UserStoppedPlayingItemWithValues": "{2}তে {1} বাজানো শেষ করেছেন {0}",
     "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
     "UserStartedPlayingItemWithValues": "{2}তে {1} বাজাচ্ছেন {0}",
@@ -36,10 +36,10 @@
     "User": "ব্যবহারকারী",
     "User": "ব্যবহারকারী",
     "TvShows": "টিভি শোগুলো",
     "TvShows": "টিভি শোগুলো",
     "System": "সিস্টেম",
     "System": "সিস্টেম",
-    "Sync": "সিংক",
+    "Sync": "সমলয় স্থাপন",
     "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
     "SubtitleDownloadFailureFromForItem": "{2} থেকে {1} এর জন্য সাবটাইটেল ডাউনলোড ব্যর্থ",
     "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
     "StartupEmbyServerIsLoading": "জেলিফিন সার্ভার লোড হচ্ছে। দয়া করে একটু পরে আবার চেষ্টা করুন।",
-    "Songs": "গানগুলো",
+    "Songs": "সঙ্গীতসমূহ",
     "Shows": "টিভি পর্ব",
     "Shows": "টিভি পর্ব",
     "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
     "ServerNameNeedsToBeRestarted": "{0} রিস্টার্ট করা প্রয়োজন",
     "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
     "ScheduledTaskStartedWithName": "{0} শুরু হয়েছে",
@@ -49,8 +49,8 @@
     "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
     "PluginUninstalledWithName": "{0} বাদ দেয়া হয়েছে",
     "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
     "PluginInstalledWithName": "{0} ইন্সটল করা হয়েছে",
     "Plugin": "প্লাগিন",
     "Plugin": "প্লাগিন",
-    "Playlists": "প্লেলিস্ট",
-    "Photos": "ছবিগুলো",
+    "Playlists": "প্লে লিস্ট সমূহ",
+    "Photos": "চিত্রসমূহ",
     "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
     "NotificationOptionVideoPlaybackStopped": "ভিডিও চলা বন্ধ",
     "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
     "NotificationOptionVideoPlayback": "ভিডিও চলা শুরু হয়েছে",
     "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
     "NotificationOptionUserLockedOut": "ব্যবহারকারী ঢুকতে পারছে না",
@@ -71,9 +71,9 @@
     "NameSeasonUnknown": "সিজন অজানা",
     "NameSeasonUnknown": "সিজন অজানা",
     "NameSeasonNumber": "সিজন {0}",
     "NameSeasonNumber": "সিজন {0}",
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
     "NameInstallFailed": "{0} ইন্সটল ব্যর্থ",
-    "MusicVideos": "গানের ভিডিও",
+    "MusicVideos": "সঙ্গীত ভিডিয়ো সমূহ",
     "Music": "গান",
     "Music": "গান",
-    "Movies": "চলচ্চিত্র",
+    "Movies": "চলচ্চিত্রসমূহ",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MixedContent": "মিশ্র কন্টেন্ট",
     "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
     "MessageServerConfigurationUpdated": "সার্ভারের কনফিগারেশন আপডেট করা হয়েছে",
     "HeaderRecordingGroups": "রেকর্ডিং দল",
     "HeaderRecordingGroups": "রেকর্ডিং দল",
@@ -117,5 +117,11 @@
     "Forced": "জোরকরে",
     "Forced": "জোরকরে",
     "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
     "TaskCleanActivityLogDescription": "নির্ধারিত সময়ের আগের কাজের হিসাব মুছে দিন খালি করুন.",
     "TaskCleanActivityLog": "কাজের ফাইল খালি করুন",
     "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",
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "AuthenticationSucceededWithUserName": "{0} s'ha autenticat correctament",
     "Books": "Llibres",
     "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",
     "Channels": "Canals",
     "ChapterNameValue": "Capítol {0}",
     "ChapterNameValue": "Capítol {0}",
     "Collections": "Col·leccions",
     "Collections": "Col·leccions",
@@ -16,65 +16,65 @@
     "Folders": "Carpetes",
     "Folders": "Carpetes",
     "Genres": "Gèneres",
     "Genres": "Gèneres",
     "HeaderAlbumArtists": "Artistes de l'àlbum",
     "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ó",
     "HeaderNextUp": "A continuació",
-    "HeaderRecordingGroups": "Grups d'Enregistrament",
-    "HomeVideos": "Vídeos Domèstics",
+    "HeaderRecordingGroups": "Grups d'enregistrament",
+    "HomeVideos": "Vídeos domèstics",
     "Inherit": "Hereta",
     "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}",
     "LabelIpAddressValue": "Adreça IP: {0}",
     "LabelRunningTimeValue": "Temps en funcionament: {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",
     "MessageNamedServerConfigurationUpdatedWithValue": "La secció {0} de la configuració del servidor ha estat actualitzada",
     "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
     "MessageServerConfigurationUpdated": "S'ha actualitzat la configuració del servidor",
     "MixedContent": "Contingut barrejat",
     "MixedContent": "Contingut barrejat",
     "Movies": "Pel·lícules",
     "Movies": "Pel·lícules",
     "Music": "Música",
     "Music": "Música",
-    "MusicVideos": "Vídeos Musicals",
+    "MusicVideos": "Videoclips",
     "NameInstallFailed": "{0} instal·lació fallida",
     "NameInstallFailed": "{0} instal·lació fallida",
     "NameSeasonNumber": "Temporada {0}",
     "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",
     "NotificationOptionAudioPlayback": "Reproducció d'àudio iniciada",
     "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
     "NotificationOptionAudioPlaybackStopped": "Reproducció d'àudio aturada",
     "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
     "NotificationOptionCameraImageUploaded": "Imatge de càmera pujada",
     "NotificationOptionInstallationFailed": "Instal·lació fallida",
     "NotificationOptionInstallationFailed": "Instal·lació fallida",
     "NotificationOptionNewLibraryContent": "Nou contingut afegit",
     "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",
     "NotificationOptionServerRestartRequired": "Reinici del servidor requerit",
     "NotificationOptionTaskFailed": "Tasca programada fallida",
     "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",
     "Photos": "Fotos",
     "Playlists": "Llistes de reproducció",
     "Playlists": "Llistes de reproducció",
-    "Plugin": "Connector",
+    "Plugin": "Complement",
     "PluginInstalledWithName": "{0} ha estat instal·lat",
     "PluginInstalledWithName": "{0} ha estat instal·lat",
     "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
     "PluginUninstalledWithName": "{0} ha estat desinstal·lat",
     "PluginUpdatedWithName": "{0} ha estat actualitzat",
     "PluginUpdatedWithName": "{0} ha estat actualitzat",
     "ProviderValue": "Proveïdor: {0}",
     "ProviderValue": "Proveïdor: {0}",
     "ScheduledTaskFailedWithName": "{0} ha fallat",
     "ScheduledTaskFailedWithName": "{0} ha fallat",
-    "ScheduledTaskStartedWithName": "{0} iniciat",
+    "ScheduledTaskStartedWithName": "{0} s'ha iniciat",
     "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
     "ServerNameNeedsToBeRestarted": "{0} necessita ser reiniciat",
     "Shows": "Sèries",
     "Shows": "Sèries",
     "Songs": "Cançons",
     "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}",
     "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",
     "Sync": "Sincronitzar",
     "System": "Sistema",
     "System": "Sistema",
     "TvShows": "Sèries de TV",
     "TvShows": "Sèries de TV",
@@ -82,11 +82,11 @@
     "UserCreatedWithName": "S'ha creat l'usuari {0}",
     "UserCreatedWithName": "S'ha creat l'usuari {0}",
     "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
     "UserDeletedWithName": "L'usuari {0} ha estat eliminat",
     "UserDownloadingItemWithValues": "{0} està descarregant {1}",
     "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}",
     "UserOfflineFromDevice": "{0} s'ha desconnectat de {1}",
     "UserOnlineFromDevice": "{0} està connectat des de {1}",
     "UserOnlineFromDevice": "{0} està connectat des de {1}",
     "UserPasswordChangedWithName": "La contrasenya ha estat canviada per a l'usuari {0}",
     "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}",
     "UserStartedPlayingItemWithValues": "{0} ha començat a reproduir {1}",
     "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
     "UserStoppedPlayingItemWithValues": "{0} ha parat de reproduir {1}",
     "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
     "ValueHasBeenAddedToLibrary": "{0} ha sigut afegit a la teva biblioteca",
@@ -94,14 +94,14 @@
     "VersionNumber": "Versió {0}",
     "VersionNumber": "Versió {0}",
     "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
     "TaskDownloadMissingSubtitlesDescription": "Cerca a internet els subtítols que faltin a partir de la configuració de metadades.",
     "TaskDownloadMissingSubtitles": "Descarrega els subtítols que faltin",
     "TaskDownloadMissingSubtitles": "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",
     "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.",
     "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.",
     "TaskCleanLogsDescription": "Esborra els logs que tinguin més de {0} dies.",
     "TaskCleanLogs": "Neteja els registres",
     "TaskCleanLogs": "Neteja els registres",
     "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
     "TaskRefreshLibraryDescription": "Escaneja la mediateca buscant fitxers nous i refresca les metadades.",
@@ -110,12 +110,12 @@
     "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
     "TaskRefreshChapterImages": "Extreure les imatges dels capítols",
     "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
     "TaskCleanCacheDescription": "Elimina els arxius temporals que ja no són necessaris per al servidor.",
     "TaskCleanCache": "Elimina arxius temporals",
     "TaskCleanCache": "Elimina arxius temporals",
-    "TasksChannelsCategory": "Canals d'Internet",
+    "TasksChannelsCategory": "Canals d'internet",
     "TasksApplicationCategory": "Aplicació",
     "TasksApplicationCategory": "Aplicació",
     "TasksLibraryCategory": "Biblioteca",
     "TasksLibraryCategory": "Biblioteca",
     "TasksMaintenanceCategory": "Manteniment",
     "TasksMaintenanceCategory": "Manteniment",
     "TaskCleanActivityLogDescription": "Eliminat entrades del registre d'activitats mes antigues que l'antiguitat configurada.",
     "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",
     "Undefined": "Indefinit",
     "Forced": "Forçat",
     "Forced": "Forçat",
     "Default": "Per defecte",
     "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.",
     "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",
     "TaskKeyframeExtractor": "Extractor de fotogrames clau",
     "External": "Extern",
     "External": "Extern",
-    "HearingImpaired": "Discapacitat Auditiva"
+    "HearingImpaired": "Discapacitat auditiva"
 }
 }

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

@@ -28,7 +28,7 @@
     "NameSeasonNumber": "Tymor {0}",
     "NameSeasonNumber": "Tymor {0}",
     "MusicVideos": "Fideos Cerddoriaeth",
     "MusicVideos": "Fideos Cerddoriaeth",
     "MixedContent": "Cynnwys amrywiol",
     "MixedContent": "Cynnwys amrywiol",
-    "HomeVideos": "Fideos Cartref",
+    "HomeVideos": "Genres",
     "HeaderNextUp": "Nesaf i Fyny",
     "HeaderNextUp": "Nesaf i Fyny",
     "HeaderFavoriteArtists": "Ffefryn Artistiaid",
     "HeaderFavoriteArtists": "Ffefryn Artistiaid",
     "HeaderFavoriteAlbums": "Ffefryn Albwmau",
     "HeaderFavoriteAlbums": "Ffefryn Albwmau",
@@ -122,5 +122,6 @@
     "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
     "TaskRefreshChapterImagesDescription": "Creu mân-luniau ar gyfer fideos sydd â phenodau.",
     "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
     "TaskRefreshChapterImages": "Echdynnu Lluniau Pennod",
     "TaskCleanCacheDescription": "Dileu ffeiliau cache nad oes eu hangen ar y system mwyach.",
     "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}",
     "AppDeviceValues": "App: {0}, Enhed: {1}",
     "Application": "Applikation",
     "Application": "Applikation",
     "Artists": "Kunstnere",
     "Artists": "Kunstnere",
-    "AuthenticationSucceededWithUserName": "{0} succesfuldt autentificeret",
+    "AuthenticationSucceededWithUserName": "{0} er logget ind",
     "Books": "Bøger",
     "Books": "Bøger",
     "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
     "CameraImageUploadedFrom": "Et nyt kamerabillede er blevet uploadet fra {0}",
     "Channels": "Kanaler",
     "Channels": "Kanaler",
@@ -11,17 +11,17 @@
     "Collections": "Samlinger",
     "Collections": "Samlinger",
     "DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
     "DeviceOfflineWithName": "{0} har afbrudt forbindelsen",
     "DeviceOnlineWithName": "{0} er forbundet",
     "DeviceOnlineWithName": "{0} er forbundet",
-    "FailedLoginAttemptWithUserName": "Fejlet loginforsøg fra {0}",
+    "FailedLoginAttemptWithUserName": "Mislykket loginforsøg fra {0}",
     "Favorites": "Favoritter",
     "Favorites": "Favoritter",
     "Folders": "Mapper",
     "Folders": "Mapper",
     "Genres": "Genrer",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Albumkunstner",
+    "HeaderAlbumArtists": "Albums kunstnere",
     "HeaderContinueWatching": "Fortsæt afspilning",
     "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",
     "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Næste",
     "HeaderNextUp": "Næste",
     "HeaderRecordingGroups": "Optagelsesgrupper",
     "HeaderRecordingGroups": "Optagelsesgrupper",
@@ -39,90 +39,90 @@
     "MixedContent": "Blandet indhold",
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Movies": "Film",
     "Music": "Musik",
     "Music": "Musik",
-    "MusicVideos": "Musik videoer",
+    "MusicVideos": "Musikvideoer",
     "NameInstallFailed": "{0} installationen mislykkedes",
     "NameInstallFailed": "{0} installationen mislykkedes",
     "NameSeasonNumber": "Sæson {0}",
     "NameSeasonNumber": "Sæson {0}",
     "NameSeasonUnknown": "Ukendt sæson",
     "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",
     "NotificationOptionAudioPlayback": "Lydafspilning påbegyndt",
     "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
     "NotificationOptionAudioPlaybackStopped": "Lydafspilning stoppet",
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
-    "NotificationOptionInstallationFailed": "Installationen fejlede",
+    "NotificationOptionInstallationFailed": "Installationen mislykkedes",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
     "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",
     "NotificationOptionVideoPlayback": "Videoafspilning påbegyndt",
-    "NotificationOptionVideoPlaybackStopped": "Videoafspilning stoppet",
-    "Photos": "Fotoer",
+    "NotificationOptionVideoPlaybackStopped": "Videoafspilning blev stoppet",
+    "Photos": "Fotos",
     "Playlists": "Afspilningslister",
     "Playlists": "Afspilningslister",
     "Plugin": "Plugin",
     "Plugin": "Plugin",
     "PluginInstalledWithName": "{0} blev installeret",
     "PluginInstalledWithName": "{0} blev installeret",
     "PluginUninstalledWithName": "{0} blev afinstalleret",
     "PluginUninstalledWithName": "{0} blev afinstalleret",
     "PluginUpdatedWithName": "{0} blev opdateret",
     "PluginUpdatedWithName": "{0} blev opdateret",
     "ProviderValue": "Udbyder: {0}",
     "ProviderValue": "Udbyder: {0}",
-    "ScheduledTaskFailedWithName": "{0} fejlet",
-    "ScheduledTaskStartedWithName": "{0} påbegyndt",
+    "ScheduledTaskFailedWithName": "{0} mislykkedes",
+    "ScheduledTaskStartedWithName": "{0} påbegyndte",
     "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
     "ServerNameNeedsToBeRestarted": "{0} skal genstartes",
     "Shows": "Serier",
     "Shows": "Serier",
     "Songs": "Sange",
     "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}",
     "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",
     "System": "System",
-    "TvShows": "Tv-serier",
+    "TvShows": "TV-serier",
     "User": "Bruger",
     "User": "Bruger",
     "UserCreatedWithName": "Bruger {0} er blevet oprettet",
     "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",
     "UserLockedOutWithName": "Brugeren {0} er blevet låst ude",
     "UserOfflineFromDevice": "{0} har afbrudt fra {1}",
     "UserOfflineFromDevice": "{0} har afbrudt fra {1}",
     "UserOnlineFromDevice": "{0} er online 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}",
     "UserStartedPlayingItemWithValues": "{0} har påbegyndt afspilning af {1}",
     "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
     "UserStoppedPlayingItemWithValues": "{0} har afsluttet afspilning af {1} på {2}",
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {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",
     "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",
     "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",
     "TasksChannelsCategory": "Internet Kanaler",
     "TasksApplicationCategory": "Applikation",
     "TasksApplicationCategory": "Applikation",
     "TasksLibraryCategory": "Bibliotek",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedligeholdelse",
     "TasksMaintenanceCategory": "Vedligeholdelse",
-    "TaskRefreshChapterImages": "Udtræk Kapitel billeder",
+    "TaskRefreshChapterImages": "Udtræk kapitel billeder",
     "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
     "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",
     "TaskCleanActivityLog": "Ryd Aktivitetslog",
     "Undefined": "Udefineret",
     "Undefined": "Udefineret",
     "Forced": "Tvunget",
     "Forced": "Tvunget",
     "Default": "Standard",
     "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",
     "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",
     "External": "Ekstern",
     "HearingImpaired": "Hørehæmmet"
     "HearingImpaired": "Hørehæmmet"
 }
 }

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

@@ -118,11 +118,11 @@
     "TaskCleanActivityLog": "Borrar log de actividades",
     "TaskCleanActivityLog": "Borrar log de actividades",
     "Undefined": "Indefinido",
     "Undefined": "Indefinido",
     "Forced": "Forzado",
     "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.",
     "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",
     "TaskOptimizeDatabase": "Optimización de base de datos",
     "External": "Externo",
     "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.",
     "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
     "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
     "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
-    "HearingImpaired": "Personas con discapacidad auditiva"
+    "HearingImpaired": "Discapacidad Auditiva"
 }
 }

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

@@ -31,7 +31,7 @@
     "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
     "ItemRemovedWithName": "{0} ha sido eliminado de la biblioteca",
     "LabelIpAddressValue": "Dirección IP: {0}",
     "LabelIpAddressValue": "Dirección IP: {0}",
     "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
     "LabelRunningTimeValue": "Tiempo de funcionamiento: {0}",
-    "Latest": "Últimos",
+    "Latest": "Últimas",
     "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
     "MessageApplicationUpdated": "Se ha actualizado el servidor Jellyfin",
     "MessageApplicationUpdatedTo": "Se ha actualizado el servidor Jellyfin a la versión {0}",
     "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",
     "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",
     "TaskOptimizeDatabase": "Optimizar base de datos",
     "External": "Externo",
     "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.",
     "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": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
     "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.",
     "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
     "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
     "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
     "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.",
     "TaskCleanActivityLogDescription": "Poistaa määritettyä ikää vanhemmat tapahtumat toimintahistoriasta.",
     "TaskCleanActivityLog": "Tyhjennä toimintahistoria",
     "TaskCleanActivityLog": "Tyhjennä toimintahistoria",
     "Undefined": "Määrittelemätön",
     "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",
     "TaskOptimizeDatabase": "Optimoi tietokanta",
     "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
     "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
     "TaskKeyframeExtractor": "Avainkuvien purkain",
     "TaskKeyframeExtractor": "Avainkuvien purkain",

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

@@ -119,5 +119,9 @@
     "Undefined": "Hindi tiyak",
     "Undefined": "Hindi tiyak",
     "Forced": "Sapilitan",
     "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.",
     "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",
     "Albums": "Alben",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
     "AppDeviceValues": "App: {0}, Gerät: {1}",
-    "Application": "Anwendung",
+    "Application": "Applikation",
     "Artists": "Künstler",
     "Artists": "Künstler",
     "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
     "AuthenticationSucceededWithUserName": "{0} hat sich angemeldet",
     "Books": "Bücher",
     "Books": "Bücher",
@@ -14,7 +14,7 @@
     "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
     "FailedLoginAttemptWithUserName": "Fehlgeschlagener Anmeldeversuch von {0}",
     "Favorites": "Favoriten",
     "Favorites": "Favoriten",
     "Folders": "Ordner",
     "Folders": "Ordner",
-    "Genres": "Genres",
+    "Genres": "Genre",
     "HeaderAlbumArtists": "Album-Künstler",
     "HeaderAlbumArtists": "Album-Künstler",
     "HeaderContinueWatching": "weiter schauen",
     "HeaderContinueWatching": "weiter schauen",
     "HeaderFavoriteAlbums": "Lieblingsalben",
     "HeaderFavoriteAlbums": "Lieblingsalben",
@@ -49,7 +49,7 @@
     "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
     "NotificationOptionAudioPlayback": "Audiowedergab gstartet",
     "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
     "NotificationOptionAudioPlaybackStopped": "Audiwedergab gstoppt",
     "NotificationOptionCameraImageUploaded": "Foti ueglade",
     "NotificationOptionCameraImageUploaded": "Foti ueglade",
-    "NotificationOptionInstallationFailed": "Installationsfehler",
+    "NotificationOptionInstallationFailed": "Installationsfähler",
     "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
     "NotificationOptionNewLibraryContent": "Nöie Inhaut hinzuegfüegt",
     "NotificationOptionPluginError": "Plugin-Fäuer",
     "NotificationOptionPluginError": "Plugin-Fäuer",
     "NotificationOptionPluginInstalled": "Plugin installiert",
     "NotificationOptionPluginInstalled": "Plugin installiert",
@@ -120,5 +120,9 @@
     "Forced": "Erzwungen",
     "Forced": "Erzwungen",
     "Default": "Standard",
     "Default": "Standard",
     "TaskOptimizeDatabase": "Datenbank optimieren",
     "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": "प्लग-इन",
     "Plugin": "प्लग-इन",
     "Playlists": "प्लेलिस्ट",
     "Playlists": "प्लेलिस्ट",
     "Photos": "तस्वीरें",
     "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",
     "MessageServerConfigurationUpdated": "Konfigurasi server telah diperbarui",
     "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
     "MessageNamedServerConfigurationUpdatedWithValue": "Bagian konfigurasi server {0} telah diperbarui",
     "FailedLoginAttemptWithUserName": "Gagal melakukan login dari {0}",
     "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",
     "DeviceOfflineWithName": "{0} telah terputus",
     "DeviceOnlineWithName": "{0} telah terhubung",
     "DeviceOnlineWithName": "{0} telah terhubung",
     "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",
     "NotificationOptionVideoPlaybackStopped": "Pemutaran video berhenti",

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

@@ -107,5 +107,14 @@
     "TasksApplicationCategory": "Forrit",
     "TasksApplicationCategory": "Forrit",
     "TasksLibraryCategory": "Miðlasafn",
     "TasksLibraryCategory": "Miðlasafn",
     "TasksMaintenanceCategory": "Viðhald",
     "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"
 }
 }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است