Browse Source

Merge branch 'master' into trickplay

Nick 1 year ago
parent
commit
cd662506a1
100 changed files with 1793 additions and 2179 deletions
  1. 1 0
      .ci/azure-pipelines-package.yml
  2. 12 0
      .config/dotnet-tools.json
  3. 4 4
      .github/workflows/codeql-analysis.yml
  4. 2 2
      .github/workflows/commands.yml
  5. 4 4
      .github/workflows/openapi.yml
  6. 82 0
      .github/workflows/repo-bump-version.yaml
  7. 7 6
      .github/workflows/repo-stale.yaml
  8. 3 0
      CONTRIBUTORS.md
  9. 28 27
      Directory.Packages.props
  10. 1 1
      Dockerfile
  11. 1 1
      Dockerfile.arm
  12. 1 1
      Dockerfile.arm64
  13. 1 1
      Emby.Dlna/Configuration/DlnaOptions.cs
  14. 31 33
      Emby.Dlna/Didl/DidlBuilder.cs
  15. 1 1
      Emby.Dlna/DlnaManager.cs
  16. 69 0
      Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
  17. 38 149
      Emby.Dlna/Main/DlnaEntryPoint.cs
  18. 46 82
      Emby.Dlna/PlayTo/Device.cs
  19. 31 33
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  20. 2 2
      Emby.Dlna/PlayTo/PlayToController.cs
  21. 7 9
      Emby.Dlna/PlayTo/PlayToManager.cs
  22. 6 2
      Emby.Dlna/Ssdp/DeviceDiscovery.cs
  23. 6 7
      Emby.Naming/Audio/AlbumParser.cs
  24. 9 7
      Emby.Naming/Common/NamingOptions.cs
  25. 1 1
      Emby.Naming/ExternalFiles/ExternalPathParser.cs
  26. 4 3
      Emby.Naming/TV/SeriesResolver.cs
  27. 3 4
      Emby.Naming/Video/StubResolver.cs
  28. 8 4
      Emby.Naming/Video/VideoListResolver.cs
  29. 1 1
      Emby.Photos/PhotoProvider.cs
  30. 2 4
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  31. 7 13
      Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
  32. 27 193
      Emby.Server.Implementations/ApplicationHost.cs
  33. 6 3
      Emby.Server.Implementations/Channels/ChannelManager.cs
  34. 6 5
      Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
  35. 27 114
      Emby.Server.Implementations/Data/BaseSqliteRepository.cs
  36. 0 79
      Emby.Server.Implementations/Data/ConnectionPool.cs
  37. 0 81
      Emby.Server.Implementations/Data/ManagedConnection.cs
  38. 70 263
      Emby.Server.Implementations/Data/SqliteExtensions.cs
  39. 358 456
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  40. 60 66
      Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
  41. 2 1
      Emby.Server.Implementations/Dto/DtoService.cs
  42. 1 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  43. 2 2
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  44. 58 10
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  45. 26 33
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  46. 3 2
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  47. 1 9
      Emby.Server.Implementations/IO/FileRefresher.cs
  48. 35 33
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  49. 25 33
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  50. 2 9
      Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
  51. 1 0
      Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
  52. 4 0
      Emby.Server.Implementations/Library/IgnorePatterns.cs
  53. 28 26
      Emby.Server.Implementations/Library/LibraryManager.cs
  54. 13 5
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  55. 10 5
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  56. 3 3
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  57. 1 1
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  58. 4 5
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  59. 5 5
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  60. 13 15
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  61. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  62. 9 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  63. 7 18
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  64. 6 6
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  65. 38 41
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  66. 5 7
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  67. 5 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs
  68. 2 5
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  69. 7 11
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  70. 43 1
      Emby.Server.Implementations/Localization/Core/as.json
  71. 52 0
      Emby.Server.Implementations/Localization/Core/chr.json
  72. 1 1
      Emby.Server.Implementations/Localization/Core/cs.json
  73. 24 24
      Emby.Server.Implementations/Localization/Core/da.json
  74. 2 2
      Emby.Server.Implementations/Localization/Core/es.json
  75. 7 7
      Emby.Server.Implementations/Localization/Core/fi.json
  76. 18 0
      Emby.Server.Implementations/Localization/Core/fo.json
  77. 2 2
      Emby.Server.Implementations/Localization/Core/fr.json
  78. 21 21
      Emby.Server.Implementations/Localization/Core/he.json
  79. 41 41
      Emby.Server.Implementations/Localization/Core/hu.json
  80. 35 28
      Emby.Server.Implementations/Localization/Core/is.json
  81. 121 1
      Emby.Server.Implementations/Localization/Core/kn.json
  82. 38 38
      Emby.Server.Implementations/Localization/Core/lv.json
  83. 3 1
      Emby.Server.Implementations/Localization/Core/ml.json
  84. 1 1
      Emby.Server.Implementations/Localization/Core/ms.json
  85. 2 2
      Emby.Server.Implementations/Localization/Core/nl.json
  86. 9 1
      Emby.Server.Implementations/Localization/Core/pr.json
  87. 3 3
      Emby.Server.Implementations/Localization/Core/ru.json
  88. 1 0
      Emby.Server.Implementations/Localization/Core/si.json
  89. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  90. 1 1
      Emby.Server.Implementations/Localization/Core/sl-SI.json
  91. 2 1
      Emby.Server.Implementations/Localization/Core/ta.json
  92. 3 1
      Emby.Server.Implementations/Localization/Core/th.json
  93. 24 24
      Emby.Server.Implementations/Localization/Core/tr.json
  94. 10 1
      Emby.Server.Implementations/Localization/Core/zu.json
  95. 20 17
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  96. 5 1
      Emby.Server.Implementations/Localization/Ratings/au.csv
  97. 5 0
      Emby.Server.Implementations/Localization/Ratings/de.csv
  98. 1 0
      Emby.Server.Implementations/Localization/Ratings/es.csv
  99. 1 0
      Emby.Server.Implementations/Localization/Ratings/fr.csv
  100. 6 0
      Emby.Server.Implementations/Localization/Ratings/sk.csv

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

@@ -168,6 +168,7 @@ jobs:
 - job: CollectArtifacts
   timeoutInMinutes: 20
   displayName: 'Collect Artifacts'
+  condition: succeededOrFailed()
   continueOnError: true
   dependsOn:
   - BuildPackage

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

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

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

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

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

@@ -24,7 +24,7 @@ jobs:
           reactions: '+1'
 
       - name: Checkout the latest code
-        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
           reactions: eyes
 
       - name: Checkout the latest code
-        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           token: ${{ secrets.JF_BOT_TOKEN }}
           fetch-depth: 0

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

@@ -14,7 +14,7 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -25,7 +25,7 @@ jobs:
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+        uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
         with:
           name: openapi-head
           retention-days: 14
@@ -39,7 +39,7 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -59,7 +59,7 @@ jobs:
       - name: Generate openapi.json
         run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
       - name: Upload openapi.json
-        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+        uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
         with:
           name: openapi-base
           retention-days: 14

+ 82 - 0
.github/workflows/repo-bump-version.yaml

@@ -0,0 +1,82 @@
+name: '🆙 Auto bump_version'
+
+on:
+  release:
+    types:
+      - published
+  workflow_dispatch:
+    inputs:
+      TAG_BRANCH:
+        required: true
+        description: release-x.y.z
+      NEXT_VERSION:
+        required: true
+        description: x.y.z
+
+jobs:
+  auto_bump_version:
+    runs-on: ubuntu-latest
+    if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
+    env:
+      TAG_BRANCH: ${{ github.event.release.target_commitish }}
+    steps:
+      - name: Wait for deploy checks to finish
+        uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
+        with:
+          ref: ${{ env.TAG_BRANCH }}
+          intervalSeconds: 60
+          timeoutSeconds: 3600
+
+      - name: Setup YQ
+        uses: chrisdickinson/setup-yq@latest
+        with:
+          yq-version: v4.9.8
+
+      - name: Checkout Repository
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+        with:
+          ref: ${{ env.TAG_BRANCH }}
+
+      - name: Setup EnvVars
+        run: |-
+          CURRENT_VERSION=$(yq e '.version' build.yaml)
+          CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
+          CURRENT_PATCH=${CURRENT_VERSION##*.}
+          echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
+          echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
+          echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
+          echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
+
+      - name: Run bump_version
+        run: ./bump_version ${{ env.NEXT_VERSION }}
+
+      - name: Commit Changes
+        run: |-
+          git config user.name "jellyfin-bot"
+          git config user.email "team@jellyfin.org"
+          git checkout ${{ env.TAG_BRANCH }}
+          git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+          git push origin ${{ env.TAG_BRANCH }}
+
+  manual_bump_version:
+    runs-on: ubuntu-latest
+    if: ${{ github.event_name == 'workflow_dispatch' }}
+    env:
+      TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
+      NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
+    steps:
+      - name: Checkout Repository
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+        with:
+          ref: ${{ env.TAG_BRANCH }}
+
+      - name: Run bump_version
+        run: ./bump_version ${{ env.NEXT_VERSION }}
+
+      - name: Commit Changes
+        run: |-
+          git config user.name "jellyfin-bot"
+          git config user.email "team@jellyfin.org"
+          git checkout ${{ env.TAG_BRANCH }}
+          git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
+          git push origin ${{ env.TAG_BRANCH }}

+ 7 - 6
.github/workflows/repo-stale.yaml

@@ -2,16 +2,17 @@ name: Stale Check
 
 on:
   schedule:
-    - cron: '30 1 * * *'
+    - cron: '30 */12 * * *'
   workflow_dispatch:
 
 permissions:
   issues: write
   pull-requests: write
+  actions: write
 
 jobs:
   issues:
-    name: Check issues
+    name: Check for stale issues
     runs-on: ubuntu-latest
     if: ${{ contains(github.repository, 'jellyfin/') }}
     steps:
@@ -26,11 +27,11 @@ jobs:
           exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
           stale-issue-label: stale
           stale-issue-message: |-
-            This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
+            This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.  
 
-            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).
+            If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
+          close-issue-message: |-
+            This issue was closed due to inactivity.
 
   prs-conflicts:
     name: Check PRs with merge conflicts

+ 3 - 0
CONTRIBUTORS.md

@@ -168,6 +168,8 @@
  - [RealGreenDragon](https://github.com/RealGreenDragon)
  - [ipitio](https://github.com/ipitio)
  - [TheTyrius](https://github.com/TheTyrius)
+ - [tallbl0nde](https://github.com/tallbl0nde)
+ - [sleepycatcoding](https://github.com/sleepycatcoding)
 
 # Emby Contributors
 
@@ -238,3 +240,4 @@
  - [Jakob Kukla](https://github.com/jakobkukla)
  - [Utku Özdemir](https://github.com/utkuozdemir)
  - [JPUC1143](https://github.com/Jpuc1143/)
+ - [0x25CBFC4F](https://github.com/0x25CBFC4F)

+ 28 - 27
Directory.Packages.props

@@ -10,26 +10,30 @@
     <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="BlurHashSharp.SkiaSharp" Version="1.3.0" />
+    <PackageVersion Include="BlurHashSharp" Version="1.3.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="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" />
+    <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
+    <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
+    <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
     <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.AspNetCore.Authorization" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
     <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.Data.Sqlite" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
@@ -38,14 +42,14 @@
     <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.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
     <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
     <PackageVersion Include="Microsoft.Extensions.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.NET.Test.Sdk" Version="17.7.2" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
     <PackageVersion Include="MimeTypes" Version="2.4.0" />
     <PackageVersion Include="Mono.Nat" Version="3.0.4" />
@@ -53,28 +57,25 @@
     <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.AspNetCore" Version="8.0.1" />
     <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
-    <PackageVersion Include="prometheus-net" Version="8.0.0" />
+    <PackageVersion Include="prometheus-net" Version="8.0.1" />
     <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.Settings.Configuration" Version="7.0.1" />
     <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="Serilog.Sinks.Graylog" Version="3.1.0" />
     <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="SharpFuzz" Version="2.1.1" />
+    <PackageVersion Include="SkiaSharp" Version="2.88.5" />
+    <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
+    <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
     <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.ReDoc" Version="6.5.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" />
@@ -85,8 +86,8 @@
     <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.runner.visualstudio" Version="2.5.3" />
     <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
-    <PackageVersion Include="xunit" Version="2.4.2" />
+    <PackageVersion Include="xunit" Version="2.5.3" />
   </ItemGroup>
 </Project>

+ 1 - 1
Dockerfile

@@ -4,7 +4,7 @@
 # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
 ARG DOTNET_VERSION=7.0
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Dockerfile.arm

@@ -5,7 +5,7 @@
 ARG DOTNET_VERSION=7.0
 
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Dockerfile.arm64

@@ -5,7 +5,7 @@
 ARG DOTNET_VERSION=7.0
 
 
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
 ARG JELLYFIN_WEB_VERSION=master
 RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
  && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

+ 1 - 1
Emby.Dlna/Configuration/DlnaOptions.cs

@@ -17,7 +17,7 @@ namespace Emby.Dlna.Configuration
             BlastAliveMessages = true;
             SendOnlyMatchedHost = true;
             ClientDiscoveryIntervalSeconds = 60;
-            AliveMessageIntervalSeconds = 1800;
+            AliveMessageIntervalSeconds = 180;
         }
 
         /// <summary>

+ 31 - 33
Emby.Dlna/Didl/DidlBuilder.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
         private readonly DeviceProfile _profile;
         private readonly IImageProcessor _imageProcessor;
         private readonly string _serverAddress;
-        private readonly string _accessToken;
-        private readonly User _user;
+        private readonly string? _accessToken;
+        private readonly User? _user;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
         private readonly IMediaSourceManager _mediaSourceManager;
@@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
 
         public DidlBuilder(
             DeviceProfile profile,
-            User user,
+            User? user,
             IImageProcessor imageProcessor,
             string serverAddress,
-            string accessToken,
+            string? accessToken,
             IUserDataManager userDataManager,
             ILocalizationManager localization,
             IMediaSourceManager mediaSourceManager,
@@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
             return url + "&dlnaheaders=true";
         }
 
-        public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
+        public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
         {
             var settings = new XmlWriterSettings
             {
@@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
         public void WriteItemElement(
             XmlWriter writer,
             BaseItem item,
-            User user,
-            BaseItem context,
+            User? user,
+            BaseItem? context,
             StubType? contextStubType,
             string deviceId,
             Filter filter,
-            StreamInfo streamInfo = null)
+            StreamInfo? streamInfo = null)
         {
             var clientId = GetClientId(item, null);
 
@@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
             writer.WriteFullEndElement();
         }
 
-        private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
+        private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
         {
             if (streamInfo is null)
             {
@@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
                     Profile = _profile,
                     DeviceId = deviceId,
                     MaxBitrate = _profile.MaxStreamingBitrate
-                });
+                }) ?? throw new InvalidOperationException("No optimal video stream found");
             }
 
             var targetWidth = streamInfo.TargetWidth;
@@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
 
             var mediaSource = streamInfo.MediaSource;
 
-            if (mediaSource.RunTimeTicks.HasValue)
+            if (mediaSource?.RunTimeTicks.HasValue == true)
             {
                 writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
             }
@@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
             writer.WriteFullEndElement();
         }
 
-        private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
+        private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
         {
             if (itemStubType.HasValue)
             {
@@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
         /// <param name="episode">The episode.</param>
         /// <param name="context">Current context.</param>
         /// <returns>Formatted name of the episode.</returns>
-        private string GetEpisodeDisplayName(Episode episode, BaseItem context)
+        private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
         {
             string[] components;
 
@@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
 
         private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
 
-        private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
+        private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
         {
             writer.WriteStartElement(string.Empty, "res", NsDidl);
 
@@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
                     MediaSources = sources.ToArray(),
                     Profile = _profile,
                     DeviceId = deviceId
-                });
+                }) ?? throw new InvalidOperationException("No optimal audio stream found");
             }
 
             var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
 
             var mediaSource = streamInfo.MediaSource;
 
-            if (mediaSource.RunTimeTicks.HasValue)
+            if (mediaSource?.RunTimeTicks is not null)
             {
                 writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
             }
@@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
                 // Samsung sometimes uses 1 as root
                 || string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
 
-        public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
+        public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
         {
             writer.WriteStartElement(string.Empty, "container", NsDidl);
 
@@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
             writer.WriteFullEndElement();
         }
 
-        private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
+        private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
         {
             if (!item.SupportsPositionTicksResume || item is Folder)
             {
                 return;
             }
 
-            XmlAttribute secAttribute = null;
+            XmlAttribute? secAttribute = null;
             foreach (var attribute in _profile.XmlRootAttributes)
             {
                 if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
                 }
             }
 
-            // Not a samsung device
-            if (secAttribute is null)
+            // Not a samsung device or no user data
+            if (secAttribute is null || user is null)
             {
                 return;
             }
@@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
         /// <summary>
         /// Adds fields used by both items and folders.
         /// </summary>
-        private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+        private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
         {
             // Don't filter on dc:title because not all devices will include it in the filter
             // MediaMonkey for example won't display content without a title
@@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
 
             if (item.IsDisplayedAsFolder || stubType.HasValue)
             {
-                string classType = null;
+                string? classType = null;
 
                 if (!_profile.RequiresPlainFolders)
                 {
@@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
             }
         }
 
-        private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
+        private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
         {
             AddCommonFields(item, itemStubType, context, writer, filter);
 
@@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
 
         private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
         {
-            ImageDownloadInfo imageInfo = GetImageInfo(item);
+            ImageDownloadInfo? imageInfo = GetImageInfo(item);
 
             if (imageInfo is null)
             {
@@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
             writer.WriteFullEndElement();
         }
 
-        private ImageDownloadInfo GetImageInfo(BaseItem item)
+        private ImageDownloadInfo? GetImageInfo(BaseItem item)
         {
             if (item.HasImage(ImageType.Primary))
             {
@@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
             return null;
         }
 
-        private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
+        private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
         {
             if (item is null)
             {
@@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
         private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
         {
             var imageInfo = item.GetImageInfo(type, 0);
-            string tag = null;
+            string? tag = null;
 
             try
             {
@@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
         {
             internal Guid ItemId { get; set; }
 
-            internal string ImageTag { get; set; }
+            internal string? ImageTag { get; set; }
 
             internal ImageType Type { get; set; }
 
@@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
 
             internal bool IsDirectStream { get; set; }
 
-            internal string Format { get; set; }
+            internal required string Format { get; set; }
 
-            internal ItemImageInfo ItemImageInfo { get; set; }
+            internal required ItemImageInfo ItemImageInfo { get; set; }
         }
     }
 }

+ 1 - 1
Emby.Dlna/DlnaManager.cs

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

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

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

+ 38 - 149
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -11,7 +11,7 @@ using System.Threading.Tasks;
 using Emby.Dlna.PlayTo;
 using Emby.Dlna.Ssdp;
 using Jellyfin.Networking.Configuration;
-using Jellyfin.Networking.Manager;
+using Jellyfin.Networking.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Common.Net;
@@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.MediaEncoding;
 using MediaBrowser.Controller.Plugins;
 using MediaBrowser.Controller.Session;
-using MediaBrowser.Controller.TV;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.Net;
 using Microsoft.Extensions.Logging;
 using Rssdp;
 using Rssdp.Infrastructure;
@@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IDeviceDiscovery _deviceDiscovery;
-        private readonly ISocketFactory _socketFactory;
+        private readonly ISsdpCommunicationsServer _communicationsServer;
         private readonly INetworkManager _networkManager;
-        private readonly object _syncLock = new object();
+        private readonly object _syncLock = new();
         private readonly bool _disabled;
 
         private PlayToManager _manager;
         private SsdpDevicePublisher _publisher;
-        private ISsdpCommunicationsServer _communicationsServer;
 
         private bool _disposed;
 
@@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
             IMediaSourceManager mediaSourceManager,
             IDeviceDiscovery deviceDiscovery,
             IMediaEncoder mediaEncoder,
-            ISocketFactory socketFactory,
-            INetworkManager networkManager,
-            IUserViewManager userViewManager,
-            ITVSeriesManager tvSeriesManager)
+            ISsdpCommunicationsServer communicationsServer,
+            INetworkManager networkManager)
         {
             _config = config;
             _appHost = appHost;
@@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
             _mediaSourceManager = mediaSourceManager;
             _deviceDiscovery = deviceDiscovery;
             _mediaEncoder = mediaEncoder;
-            _socketFactory = socketFactory;
+            _communicationsServer = communicationsServer;
             _networkManager = networkManager;
             _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
 
-            ContentDirectory = new ContentDirectory.ContentDirectoryService(
-                dlnaManager,
-                userDataManager,
-                imageProcessor,
-                libraryManager,
-                config,
-                userManager,
-                loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
-                httpClientFactory,
-                localizationManager,
-                mediaSourceManager,
-                userViewManager,
-                mediaEncoder,
-                tvSeriesManager);
-
-            ConnectionManager = new ConnectionManager.ConnectionManagerService(
-                dlnaManager,
-                config,
-                loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
-                httpClientFactory);
-
-            MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
-                loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
-                httpClientFactory,
-                config);
-            Current = this;
-
             var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
             _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
 
@@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
             }
         }
 
-        public static DlnaEntryPoint Current { get; private set; }
-
-        /// <summary>
-        /// Gets a value indicating whether the dlna server is enabled.
-        /// </summary>
-        public static bool Enabled { get; private set; }
-
-        public IContentDirectory ContentDirectory { get; private set; }
-
-        public IConnectionManager ConnectionManager { get; private set; }
-
-        public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
-
         public async Task RunAsync()
         {
             await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
         private void ReloadComponents()
         {
             var options = _config.GetDlnaConfiguration();
-            Enabled = options.EnableServer;
-
-            StartSsdpHandler();
+            StartDeviceDiscovery();
 
             if (options.EnableServer)
             {
@@ -195,37 +148,11 @@ namespace Emby.Dlna.Main
             }
         }
 
-        private void StartSsdpHandler()
-        {
-            try
-            {
-                if (_communicationsServer is null)
-                {
-                    var enableMultiSocketBinding = OperatingSystem.IsWindows() ||
-                                                   OperatingSystem.IsLinux();
-
-                    _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
-                    {
-                        IsShared = true
-                    };
-
-                    StartDeviceDiscovery(_communicationsServer);
-                }
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error starting ssdp handlers");
-            }
-        }
-
-        private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
+        private void StartDeviceDiscovery()
         {
             try
             {
-                if (communicationsServer is not null)
-                {
-                    ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
-                }
+                ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
             }
             catch (Exception ex)
             {
@@ -233,26 +160,8 @@ namespace Emby.Dlna.Main
             }
         }
 
-        private void DisposeDeviceDiscovery()
-        {
-            try
-            {
-                _logger.LogInformation("Disposing DeviceDiscovery");
-                ((DeviceDiscovery)_deviceDiscovery).Dispose();
-            }
-            catch (Exception ex)
-            {
-                _logger.LogError(ex, "Error stopping device discovery");
-            }
-        }
-
         public void StartDevicePublisher(Configuration.DlnaOptions options)
         {
-            if (!options.BlastAliveMessages)
-            {
-                return;
-            }
-
             if (_publisher is not null)
             {
                 return;
@@ -263,7 +172,8 @@ namespace Emby.Dlna.Main
                 _publisher = new SsdpDevicePublisher(
                     _communicationsServer,
                     Environment.OSVersion.Platform.ToString(),
-                    Environment.OSVersion.VersionString,
+                    // Can not use VersionString here since that includes OS and version
+                    Environment.OSVersion.Version.ToString(),
                     _config.GetDlnaConfiguration().SendOnlyMatchedHost)
                 {
                     LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
@@ -272,7 +182,10 @@ namespace Emby.Dlna.Main
 
                 RegisterServerEndpoints();
 
-                _publisher.StartBroadcastingAliveMessages(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+                if (options.BlastAliveMessages)
+                {
+                    _publisher.StartSendingAliveNotifications(TimeSpan.FromSeconds(options.BlastAliveMessageIntervalSeconds));
+                }
             }
             catch (Exception ex)
             {
@@ -285,42 +198,33 @@ namespace Emby.Dlna.Main
             var udn = CreateUuid(_appHost.SystemId);
             var descriptorUri = "/dlna/" + udn + "/description.xml";
 
-            var bindAddresses = NetworkManager.CreateCollection(
-                _networkManager.GetInternalBindAddresses()
-                .Where(i => i.AddressFamily == AddressFamily.InterNetwork || (i.AddressFamily == AddressFamily.InterNetworkV6 && i.Address.ScopeId != 0)));
+            // Only get bind addresses in LAN
+            // IPv6 is currently unsupported
+            var validInterfaces = _networkManager.GetInternalBindAddresses()
+                .Where(x => x.Address is not null)
+                .Where(x => x.AddressFamily != AddressFamily.InterNetworkV6)
+                .ToList();
 
-            if (bindAddresses.Count == 0)
+            if (validInterfaces.Count == 0)
             {
-                // No interfaces returned, so use loopback.
-                bindAddresses = _networkManager.GetLoopbacks();
+                // No interfaces returned, fall back to loopback
+                validInterfaces = _networkManager.GetLoopbacks().ToList();
             }
 
-            foreach (IPNetAddress address in bindAddresses)
+            foreach (var intf in validInterfaces)
             {
-                if (address.AddressFamily == AddressFamily.InterNetworkV6)
-                {
-                    // Not supporting IPv6 right now
-                    continue;
-                }
-
-                // Limit to LAN addresses only
-                if (!_networkManager.IsInLocalNetwork(address))
-                {
-                    continue;
-                }
-
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
-                _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
+                _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, intf.Address);
 
-                var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(address, false) + descriptorUri);
+                var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(intf.Address, false) + descriptorUri);
 
                 var device = new SsdpRootDevice
                 {
                     CacheLifetime = TimeSpan.FromSeconds(1800), // How long SSDP clients can cache this info.
                     Location = uri.Uri, // Must point to the URL that serves your devices UPnP description document.
-                    Address = address.Address,
-                    PrefixLength = address.PrefixLength,
+                    Address = intf.Address,
+                    PrefixLength = NetworkExtensions.MaskToCidr(intf.Subnet.Prefix),
                     FriendlyName = "Jellyfin",
                     Manufacturer = "Jellyfin",
                     ModelName = "Jellyfin Server",
@@ -328,7 +232,7 @@ namespace Emby.Dlna.Main
                     // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                 };
 
-                SetProperies(device, fullService);
+                SetProperties(device, fullService);
                 _publisher.AddDevice(device);
 
                 var embeddedDevices = new[]
@@ -349,13 +253,13 @@ namespace Emby.Dlna.Main
                         // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
                     };
 
-                    SetProperies(embeddedDevice, subDevice);
+                    SetProperties(embeddedDevice, subDevice);
                     device.AddDevice(embeddedDevice);
                 }
             }
         }
 
-        private string CreateUuid(string text)
+        private static string CreateUuid(string text)
         {
             if (!Guid.TryParse(text, out var guid))
             {
@@ -365,15 +269,14 @@ namespace Emby.Dlna.Main
             return guid.ToString("D", CultureInfo.InvariantCulture);
         }
 
-        private void SetProperies(SsdpDevice device, string fullDeviceType)
+        private static void SetProperties(SsdpDevice device, string fullDeviceType)
         {
-            var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase);
-
-            var serviceParts = service.Split(':');
+            var serviceParts = fullDeviceType
+                .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Split(':');
 
-            var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
-
-            device.DeviceTypeNamespace = deviceTypeNamespace;
+            device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
             device.DeviceClass = serviceParts[1];
             device.DeviceType = serviceParts[2];
         }
@@ -454,20 +357,6 @@ namespace Emby.Dlna.Main
 
             DisposeDevicePublisher();
             DisposePlayToManager();
-            DisposeDeviceDiscovery();
-
-            if (_communicationsServer is not null)
-            {
-                _logger.LogInformation("Disposing SsdpCommunicationsServer");
-                _communicationsServer.Dispose();
-                _communicationsServer = null;
-            }
-
-            ContentDirectory = null;
-            ConnectionManager = null;
-            MediaReceiverRegistrar = null;
-            Current = null;
-
             _disposed = true;
         }
     }

+ 46 - 82
Emby.Dlna/PlayTo/Device.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
         private readonly ILogger _logger;
 
         private readonly object _timerLock = new object();
-        private Timer _timer;
+        private Timer? _timer;
         private int _muteVol;
         private int _volume;
         private DateTime _lastVolumeRefresh;
@@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
             _logger = logger;
         }
 
-        public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
+        public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
 
-        public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
+        public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
 
-        public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
+        public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
 
-        public event EventHandler<MediaChangedEventArgs> MediaChanged;
+        public event EventHandler<MediaChangedEventArgs>? MediaChanged;
 
         public DeviceInfo Properties { get; set; }
 
@@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
 
         public bool IsStopped => TransportState == TransportState.STOPPED;
 
-        public Action OnDeviceUnavailable { get; set; }
+        public Action? OnDeviceUnavailable { get; set; }
 
-        private TransportCommands AvCommands { get; set; }
+        private TransportCommands? AvCommands { get; set; }
 
-        private TransportCommands RendererCommands { get; set; }
+        private TransportCommands? RendererCommands { get; set; }
 
-        public UBaseObject CurrentMediaInfo { get; private set; }
+        public UBaseObject? CurrentMediaInfo { get; private set; }
 
         public void Start()
         {
@@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
                 _volumeRefreshActive = true;
 
                 var time = immediate ? 100 : 10000;
-                _timer.Change(time, Timeout.Infinite);
+                _timer?.Change(time, Timeout.Infinite);
             }
         }
 
@@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
 
                 _volumeRefreshActive = false;
 
-                _timer.Change(Timeout.Infinite, Timeout.Infinite);
+                _timer?.Change(Timeout.Infinite, Timeout.Infinite);
             }
         }
 
@@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
             }
         }
 
-        private DeviceService GetServiceRenderingControl()
+        private DeviceService? GetServiceRenderingControl()
         {
             var services = Properties.Services;
 
@@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
                 services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
         }
 
-        private DeviceService GetAvTransportService()
+        private DeviceService? GetAvTransportService()
         {
             var services = Properties.Services;
 
@@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
                     Properties.BaseUrl,
                     service,
                     command.Name,
-                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
                     cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
@@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var service = GetServiceRenderingControl();
-
-            if (service is null)
-            {
-                throw new InvalidOperationException("Unable to find service");
-            }
+            var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
 
             // Set it early and assume it will succeed
             // Remote control will perform better
@@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
                     Properties.BaseUrl,
                     service,
                     command.Name,
-                    rendererCommands.BuildPost(command, service.ServiceType, value),
+                    rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
                     cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
         }
@@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var service = GetAvTransportService();
-
-            if (service is null)
-            {
-                throw new InvalidOperationException("Unable to find service");
-            }
-
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
             await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
                     command.Name,
-                    avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
+                    avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
                     cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
             RestartTimer(true);
         }
 
-        public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
+        public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
         {
             var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
 
@@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
                 { "CurrentURIMetaData", CreateDidlMeta(metaData) }
             };
 
-            var service = GetAvTransportService();
-
-            if (service is null)
-            {
-                throw new InvalidOperationException("Unable to find service");
-            }
-
-            var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+            var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
             await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
@@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
          * SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
          * Without that information, the next track command on the device does not work.
          */
-        public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
+        public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
         {
             var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
 
@@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
 
             _logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
 
-            var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
+            var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
             if (command is null)
             {
                 return;
@@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
                 { "NextURIMetaData", CreateDidlMeta(metaData) }
             };
 
-            var service = GetAvTransportService();
-
-            if (service is null)
-            {
-                throw new InvalidOperationException("Unable to find service");
-            }
-
-            var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
+            var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
             await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
                 .ConfigureAwait(false);
@@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
                 return Task.CompletedTask;
             }
 
-            var service = GetAvTransportService();
-            if (service is null)
-            {
-                throw new InvalidOperationException("Unable to find service");
-            }
-
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
             return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
                 Properties.BaseUrl,
                 service,
@@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var service = GetAvTransportService();
-
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
             await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
                     command.Name,
-                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
                     cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
@@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
                 return;
             }
 
-            var service = GetAvTransportService();
-
+            var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
             await new DlnaHttpClient(_logger, _httpClientFactory)
                 .SendCommandAsync(
                     Properties.BaseUrl,
                     service,
                     command.Name,
-                    avCommands.BuildPost(command, service.ServiceType, 1),
+                    avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
                     cancellationToken: cancellationToken)
                 .ConfigureAwait(false);
 
@@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
             RestartTimer(true);
         }
 
-        private async void TimerCallback(object sender)
+        private async void TimerCallback(object? sender)
         {
             if (_disposed)
             {
@@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
                 Properties.BaseUrl,
                 service,
                 command.Name,
-                rendererCommands.BuildPost(command, service.ServiceType),
+                rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
                 cancellationToken: cancellationToken).ConfigureAwait(false);
 
             if (result is null || result.Document is null)
@@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
                 Properties.BaseUrl,
                 service,
                 command.Name,
-                rendererCommands.BuildPost(command, service.ServiceType),
+                rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
                 cancellationToken: cancellationToken).ConfigureAwait(false);
 
             if (result is null || result.Document is null)
@@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
             if (command is null)
@@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+        private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
         {
             var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
             if (command is null)
@@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
                 return (true, null);
             }
 
-            XElement uPnpResponse = null;
+            XElement? uPnpResponse = null;
 
             try
             {
@@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
             return (true, uTrack);
         }
 
-        private XElement ParseResponse(string xml)
+        private XElement? ParseResponse(string xml)
         {
             // Handle different variations sent back by devices.
             try
@@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
             return null;
         }
 
-        private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
+        private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
         {
             ArgumentNullException.ThrowIfNull(container);
 
@@ -959,20 +927,17 @@ namespace Emby.Dlna.PlayTo
 
             var resElement = container.Element(UPnpNamespaces.Res);
 
-            if (resElement is not null)
-            {
-                var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
+            var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
 
-                if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
-                {
-                    return info.Value.Split(':');
-                }
+            if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
+            {
+                return info.Value.Split(':');
             }
 
             return new string[4];
         }
 
-        private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
+        private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
         {
             if (AvCommands is not null)
             {
@@ -1004,7 +969,7 @@ namespace Emby.Dlna.PlayTo
             return AvCommands;
         }
 
-        private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
+        private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
         {
             if (RendererCommands is not null)
             {
@@ -1054,7 +1019,7 @@ namespace Emby.Dlna.PlayTo
             return baseUrl + url;
         }
 
-        public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
+        public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
         {
             var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
 
@@ -1171,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
             return new Device(deviceProperties, httpClientFactory, logger);
         }
 
-#nullable enable
         private static DeviceIcon CreateIcon(XElement element)
         {
             ArgumentNullException.ThrowIfNull(element);
@@ -1287,7 +1251,7 @@ namespace Emby.Dlna.PlayTo
             }
 
             _timer = null;
-            Properties = null;
+            Properties = null!;
 
             _disposed = true;
         }

+ 31 - 33
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -31,6 +31,9 @@ namespace Emby.Dlna.PlayTo
             _httpClientFactory = httpClientFactory;
         }
 
+        [GeneratedRegex("(&(?![a-z]*;))")]
+        private static partial Regex EscapeAmpersandRegex();
+
         private static string NormalizeServiceUrl(string baseUrl, string serviceUrl)
         {
             // If it's already a complete url, don't stick anything onto the front of it
@@ -52,40 +55,42 @@ namespace Emby.Dlna.PlayTo
             var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
             using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
             response.EnsureSuccessStatusCode();
-            await using MemoryStream ms = new MemoryStream();
-            await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
-            try
+            Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            await using (stream.ConfigureAwait(false))
             {
-                return await XDocument.LoadAsync(
-                    ms,
-                    LoadOptions.None,
-                    cancellationToken).ConfigureAwait(false);
-            }
-            catch (XmlException)
-            {
-                // try correcting the Xml response with common errors
-                ms.Position = 0;
-                using StreamReader sr = new StreamReader(ms);
-                var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
-
-                // find and replace unescaped ampersands (&)
-                xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
-
                 try
                 {
-                    // retry reading Xml
-                    using var xmlReader = new StringReader(xmlString);
                     return await XDocument.LoadAsync(
-                        xmlReader,
+                        stream,
                         LoadOptions.None,
                         cancellationToken).ConfigureAwait(false);
                 }
-                catch (XmlException ex)
+                catch (XmlException)
                 {
-                    _logger.LogError(ex, "Failed to parse response");
-                    _logger.LogDebug("Malformed response: {Content}\n", xmlString);
-
-                    return null;
+                    // try correcting the Xml response with common errors
+                    stream.Position = 0;
+                    using StreamReader sr = new StreamReader(stream);
+                    var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
+
+                    // find and replace unescaped ampersands (&)
+                    xmlString = EscapeAmpersandRegex().Replace(xmlString, "&amp;");
+
+                    try
+                    {
+                        // retry reading Xml
+                        using var xmlReader = new StringReader(xmlString);
+                        return await XDocument.LoadAsync(
+                            xmlReader,
+                            LoadOptions.None,
+                            cancellationToken).ConfigureAwait(false);
+                    }
+                    catch (XmlException ex)
+                    {
+                        _logger.LogError(ex, "Failed to parse response");
+                        _logger.LogDebug("Malformed response: {Content}\n", xmlString);
+
+                        return null;
+                    }
                 }
             }
         }
@@ -128,12 +133,5 @@ namespace Emby.Dlna.PlayTo
             // Have to await here instead of returning the Task directly, otherwise request would be disposed too soon
             return await SendRequestAsync(request, cancellationToken).ConfigureAwait(false);
         }
-
-        /// <summary>
-        /// Compile-time generated regular expression for escaping ampersands.
-        /// </summary>
-        /// <returns>Compiled regular expression.</returns>
-        [GeneratedRegex("(&(?![a-z]*;))")]
-        private static partial Regex EscapeAmpersandRegex();
     }
 }

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

@@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo
 
         private readonly IDeviceDiscovery _deviceDiscovery;
         private readonly string _serverAddress;
-        private readonly string _accessToken;
+        private readonly string? _accessToken;
 
         private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
         private Device _device;
@@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
             IUserManager userManager,
             IImageProcessor imageProcessor,
             string serverAddress,
-            string accessToken,
+            string? accessToken,
             IDeviceDiscovery deviceDiscovery,
             IUserDataManager userDataManager,
             ILocalizationManager localization,

+ 7 - 9
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -41,9 +39,9 @@ namespace Emby.Dlna.PlayTo
         private readonly IMediaSourceManager _mediaSourceManager;
         private readonly IMediaEncoder _mediaEncoder;
 
+        private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
+        private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
         private bool _disposed;
-        private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
-        private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
 
         public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
         {
@@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
             _deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
         }
 
-        private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
+        private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
         {
             if (_disposed)
             {
@@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
 
             var info = e.Argument;
 
-            if (!info.Headers.TryGetValue("USN", out string usn))
+            if (!info.Headers.TryGetValue("USN", out string? usn))
             {
                 usn = string.Empty;
             }
 
-            if (!info.Headers.TryGetValue("NT", out string nt))
+            if (!info.Headers.TryGetValue("NT", out string? nt))
             {
                 nt = string.Empty;
             }
@@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
             var uri = info.Location;
             _logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
 
-            if (info.Headers.TryGetValue("USN", out string uuid))
+            if (info.Headers.TryGetValue("USN", out string? uuid))
             {
                 uuid = GetUuid(uuid);
             }
@@ -189,7 +187,7 @@ namespace Emby.Dlna.PlayTo
 
                 _sessionManager.UpdateDeviceName(sessionInfo.Id, deviceName);
 
-                string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIpAddress);
+                string serverAddress = _appHost.GetSmartApiUrl(info.RemoteIPAddress);
 
                 controller = new PlayToController(
                     sessionInfo,

+ 6 - 2
Emby.Dlna/Ssdp/DeviceDiscovery.cs

@@ -73,7 +73,11 @@ namespace Emby.Dlna.Ssdp
             {
                 if (_listenerCount > 0 && _deviceLocator is null && _commsServer is not null)
                 {
-                    _deviceLocator = new SsdpDeviceLocator(_commsServer);
+                    _deviceLocator = new SsdpDeviceLocator(
+                        _commsServer,
+                        Environment.OSVersion.Platform.ToString(),
+                        // Can not use VersionString here since that includes OS and version
+                        Environment.OSVersion.Version.ToString());
 
                     // (Optional) Set the filter so we only see notifications for devices we care about
                     // (can be any search target value i.e device type, uuid value etc - any value that appears in the
@@ -106,7 +110,7 @@ namespace Emby.Dlna.Ssdp
                 {
                     Location = e.DiscoveredDevice.DescriptionLocation,
                     Headers = headers,
-                    RemoteIpAddress = e.RemoteIpAddress
+                    RemoteIPAddress = e.RemoteIPAddress
                 });
 
             DeviceDiscoveredInternal?.Invoke(this, args);

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

@@ -10,7 +10,7 @@ namespace Emby.Naming.Audio
     /// <summary>
     /// Helper class to determine if Album is multipart.
     /// </summary>
-    public class AlbumParser
+    public partial class AlbumParser
     {
         private readonly NamingOptions _options;
 
@@ -23,6 +23,9 @@ namespace Emby.Naming.Audio
             _options = options;
         }
 
+        [GeneratedRegex(@"[-\.\(\)\s]+")]
+        private static partial Regex CleanRegex();
+
         /// <summary>
         /// Function that determines if album is multipart.
         /// </summary>
@@ -42,13 +45,9 @@ namespace Emby.Naming.Audio
 
             // Normalize
             // Remove whitespace
-            filename = filename.Replace('-', ' ');
-            filename = filename.Replace('.', ' ');
-            filename = filename.Replace('(', ' ');
-            filename = filename.Replace(')', ' ');
-            filename = Regex.Replace(filename, @"\s+", " ");
+            filename = CleanRegex().Replace(filename, " ");
 
-            ReadOnlySpan<char> trimmedFilename = filename.TrimStart();
+            ReadOnlySpan<char> trimmedFilename = filename.AsSpan().TrimStart();
 
             foreach (var prefix in _options.AlbumStackingPrefixes)
             {

+ 9 - 7
Emby.Naming/Common/NamingOptions.cs

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

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

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

+ 4 - 3
Emby.Naming/TV/SeriesResolver.cs

@@ -7,14 +7,15 @@ namespace Emby.Naming.TV
     /// <summary>
     /// Used to resolve information about series from path.
     /// </summary>
-    public static class SeriesResolver
+    public static partial class SeriesResolver
     {
         /// <summary>
         /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
         /// Used for removing separators between words, i.e turns "The_show" into "The show" while
         /// preserving namings like "S.H.O.W".
         /// </summary>
-        private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))", RegexOptions.Compiled);
+        [GeneratedRegex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))")]
+        private static partial Regex SeriesNameRegex();
 
         /// <summary>
         /// Resolve information about series from path.
@@ -37,7 +38,7 @@ namespace Emby.Naming.TV
 
             if (!string.IsNullOrEmpty(seriesName))
             {
-                seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+                seriesName = SeriesNameRegex().Replace(seriesName, "${a} ${b}").Trim();
             }
 
             return new SeriesInfo(path)

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

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

+ 8 - 4
Emby.Naming/Video/VideoListResolver.cs

@@ -12,9 +12,13 @@ namespace Emby.Naming.Video
     /// <summary>
     /// Resolves alternative versions and extras from list of video files.
     /// </summary>
-    public static class VideoListResolver
+    public static partial class VideoListResolver
     {
-        private static readonly Regex _resolutionRegex = new Regex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase | RegexOptions.Compiled);
+        [GeneratedRegex("[0-9]{2}[0-9]+[ip]", RegexOptions.IgnoreCase)]
+        private static partial Regex ResolutionRegex();
+
+        [GeneratedRegex(@"^\[([^]]*)\]")]
+        private static partial Regex CheckMultiVersionRegex();
 
         /// <summary>
         /// Resolves alternative versions and extras from list of video files.
@@ -131,7 +135,7 @@ namespace Emby.Naming.Video
 
             if (videos.Count > 1)
             {
-                var groups = videos.GroupBy(x => _resolutionRegex.IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
+                var groups = videos.GroupBy(x => ResolutionRegex().IsMatch(x.Files[0].FileNameWithoutExtension)).ToList();
                 videos.Clear();
                 foreach (var group in groups)
                 {
@@ -201,7 +205,7 @@ namespace Emby.Naming.Video
             // The CleanStringParser should have removed common keywords etc.
             return testFilename.IsEmpty
                    || testFilename[0] == '-'
-                   || Regex.IsMatch(testFilename, @"^\[([^]]*)\]", RegexOptions.Compiled);
+                   || CheckMultiVersionRegex().IsMatch(testFilename);
         }
     }
 }

+ 1 - 1
Emby.Photos/PhotoProvider.cs

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

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

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

+ 7 - 13
Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs

@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
     /// </summary>
     public abstract class BaseConfigurationManager : IConfigurationManager
     {
-        private readonly IFileSystem _fileSystem;
-
-        private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
-
-        /// <summary>
-        /// The _configuration sync lock.
-        /// </summary>
-        private readonly object _configurationSyncLock = new object();
+        private readonly ConcurrentDictionary<string, object> _configurations = new();
+        private readonly object _configurationSyncLock = new();
 
         private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
         private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
         /// <param name="applicationPaths">The application paths.</param>
         /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        /// <param name="fileSystem">The file system.</param>
-        protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+        protected BaseConfigurationManager(
+            IApplicationPaths applicationPaths,
+            ILoggerFactory loggerFactory,
+            IXmlSerializer xmlSerializer)
         {
             CommonApplicationPaths = applicationPaths;
             XmlSerializer = xmlSerializer;
-            _fileSystem = fileSystem;
             Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
 
             UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
         {
             var file = Path.Combine(path, Guid.NewGuid().ToString());
             File.WriteAllText(file, string.Empty);
-            _fileSystem.DeleteFile(file);
+            File.Delete(file);
         }
 
         private string GetConfigurationFile(string key)

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

@@ -12,11 +12,8 @@ using System.Linq;
 using System.Net;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
-using System.Threading;
 using System.Threading.Tasks;
-using Emby.Dlna;
 using Emby.Dlna.Main;
-using Emby.Dlna.Ssdp;
 using Emby.Naming.Common;
 using Emby.Photos;
 using Emby.Server.Implementations.Channels;
@@ -59,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.ClientEvent;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;
-using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
 using MediaBrowser.Controller.Entities;
@@ -83,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
 using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
-using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.MediaInfo;
@@ -112,7 +107,7 @@ namespace Emby.Server.Implementations
     /// <summary>
     /// Class CompositionRoot.
     /// </summary>
-    public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
+    public abstract class ApplicationHost : IServerApplicationHost, IDisposable
     {
         /// <summary>
         /// The disposable parts.
@@ -120,14 +115,12 @@ namespace Emby.Server.Implementations
         private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
         private readonly DeviceId _deviceId;
 
-        private readonly IFileSystem _fileSystemManager;
         private readonly IConfiguration _startupConfig;
         private readonly IXmlSerializer _xmlSerializer;
         private readonly IStartupOptions _startupOptions;
         private readonly IPluginManager _pluginManager;
 
         private List<Type> _creatingInstances;
-        private ISessionManager _sessionManager;
 
         /// <summary>
         /// Gets or sets all concrete types.
@@ -135,7 +128,7 @@ namespace Emby.Server.Implementations
         /// <value>All concrete types.</value>
         private Type[] _allConcreteTypes;
 
-        private bool _disposed = false;
+        private bool _disposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@@ -154,10 +147,8 @@ namespace Emby.Server.Implementations
             LoggerFactory = loggerFactory;
             _startupOptions = options;
             _startupConfig = startupConfig;
-            _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
-            _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
             _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
 
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@@ -165,13 +156,15 @@ namespace Emby.Server.Implementations
             ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
 
             _xmlSerializer = new MyXmlSerializer();
-            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+            ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
             _pluginManager = new PluginManager(
                 LoggerFactory.CreateLogger<PluginManager>(),
                 this,
                 ConfigurationManager.Configuration,
                 ApplicationPaths.PluginsPath,
                 ApplicationVersion);
+
+            _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
         }
 
         /// <summary>
@@ -186,23 +179,16 @@ namespace Emby.Server.Implementations
 
         public bool CoreStartupHasCompleted { get; private set; }
 
-        public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
-            && !_startupOptions.IsService
-            && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
-
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// </summary>
         public INetworkManager NetManager { get; private set; }
 
-        /// <summary>
-        /// Gets a value indicating whether this instance has changes that require the entire application to restart.
-        /// </summary>
-        /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
+        /// <inheritdoc />
         public bool HasPendingRestart { get; private set; }
 
         /// <inheritdoc />
-        public bool IsShuttingDown { get; private set; }
+        public bool ShouldRestart { get; set; }
 
         /// <summary>
         /// Gets the logger.
@@ -406,11 +392,9 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Runs the startup tasks.
         /// </summary>
-        /// <param name="cancellationToken">The cancellation token.</param>
         /// <returns><see cref="Task" />.</returns>
-        public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
+        public async Task RunStartupTasksAsync()
         {
-            cancellationToken.ThrowIfCancellationRequested();
             Logger.LogInformation("Running startup tasks");
 
             Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -424,8 +408,6 @@ namespace Emby.Server.Implementations
 
             var entryPoints = GetExports<IServerEntryPoint>();
 
-            cancellationToken.ThrowIfCancellationRequested();
-
             var stopWatch = new Stopwatch();
             stopWatch.Start();
 
@@ -435,8 +417,6 @@ namespace Emby.Server.Implementations
             Logger.LogInformation("Core startup complete");
             CoreStartupHasCompleted = true;
 
-            cancellationToken.ThrowIfCancellationRequested();
-
             stopWatch.Restart();
 
             await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -466,7 +446,7 @@ namespace Emby.Server.Implementations
 
             ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
 
-            NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
+            NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
 
             // Initialize runtime stat collection
             if (ConfigurationManager.Configuration.EnableMetrics)
@@ -475,8 +455,8 @@ namespace Emby.Server.Implementations
             }
 
             var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
-            HttpPort = networkConfiguration.HttpServerPortNumber;
-            HttpsPort = networkConfiguration.HttpsPortNumber;
+            HttpPort = networkConfiguration.InternalHttpPort;
+            HttpsPort = networkConfiguration.InternalHttpsPort;
 
             // Safeguard against invalid configuration
             if (HttpPort == HttpsPort)
@@ -509,7 +489,11 @@ namespace Emby.Server.Implementations
             serviceCollection.AddSingleton(_pluginManager);
             serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            serviceCollection.AddSingleton(_fileSystemManager);
+            serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
+            serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
+
+            serviceCollection.AddScoped<ISystemManager, SystemManager>();
+
             serviceCollection.AddSingleton<TmdbClientManager>();
 
             serviceCollection.AddSingleton(NetManager);
@@ -575,8 +559,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
-
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -588,8 +570,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
-
             serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@@ -633,8 +613,6 @@ namespace Emby.Server.Implementations
             var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
             await localizationManager.LoadAll().ConfigureAwait(false);
 
-            _sessionManager = Resolve<ISessionManager>();
-
             SetStaticProperties();
 
             FindParts();
@@ -685,7 +663,7 @@ namespace Emby.Server.Implementations
             BaseItem.ProviderManager = Resolve<IProviderManager>();
             BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
             BaseItem.ItemRepository = Resolve<IItemRepository>();
-            BaseItem.FileSystem = _fileSystemManager;
+            BaseItem.FileSystem = Resolve<IFileSystem>();
             BaseItem.UserDataManager = Resolve<IUserDataManager>();
             BaseItem.ChannelManager = Resolve<IChannelManager>();
             Video.LiveTvManager = Resolve<ILiveTvManager>();
@@ -785,8 +763,8 @@ namespace Emby.Server.Implementations
             if (HttpPort != 0 && HttpsPort != 0)
             {
                 // Need to restart if ports have changed
-                if (networkConfiguration.HttpServerPortNumber != HttpPort
-                    || networkConfiguration.HttpsPortNumber != HttpsPort)
+                if (networkConfiguration.InternalHttpPort != HttpPort
+                    || networkConfiguration.InternalHttpsPort != HttpsPort)
                 {
                     if (ConfigurationManager.Configuration.IsPortAuthorized)
                     {
@@ -855,38 +833,6 @@ namespace Emby.Server.Implementations
             }
         }
 
-        /// <summary>
-        /// Restarts this instance.
-        /// </summary>
-        public void Restart()
-        {
-            if (IsShuttingDown)
-            {
-                return;
-            }
-
-            IsShuttingDown = true;
-            _pluginManager.UnloadAssemblies();
-
-            Task.Run(async () =>
-            {
-                try
-                {
-                    await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error sending server restart notification");
-                }
-
-                Logger.LogInformation("Calling RestartInternal");
-
-                RestartInternal();
-            });
-        }
-
-        protected abstract void RestartInternal();
-
         /// <summary>
         /// Gets the composable part assemblies.
         /// </summary>
@@ -942,49 +888,6 @@ namespace Emby.Server.Implementations
 
         protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
 
-        /// <summary>
-        /// Gets the system status.
-        /// </summary>
-        /// <param name="request">Where this request originated.</param>
-        /// <returns>SystemInfo.</returns>
-        public SystemInfo GetSystemInfo(HttpRequest request)
-        {
-            return new SystemInfo
-            {
-                HasPendingRestart = HasPendingRestart,
-                IsShuttingDown = IsShuttingDown,
-                Version = ApplicationVersionString,
-                WebSocketPortNumber = HttpPort,
-                CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
-                Id = SystemId,
-                ProgramDataPath = ApplicationPaths.ProgramDataPath,
-                WebPath = ApplicationPaths.WebPath,
-                LogPath = ApplicationPaths.LogDirectoryPath,
-                ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
-                InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
-                CachePath = ApplicationPaths.CachePath,
-                CanLaunchWebBrowser = CanLaunchWebBrowser,
-                TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                SupportsLibraryMonitor = true,
-                PackageName = _startupOptions.PackageName
-            };
-        }
-
-        public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
-        {
-            return new PublicSystemInfo
-            {
-                Version = ApplicationVersionString,
-                ProductName = ApplicationProductName,
-                Id = SystemId,
-                ServerName = FriendlyName,
-                LocalAddress = GetSmartApiUrl(request),
-                StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
-            };
-        }
-
         /// <inheritdoc/>
         public string GetSmartApiUrl(IPAddress remoteAddr)
         {
@@ -995,18 +898,20 @@ namespace Emby.Server.Implementations
                 return PublishedServerUrl.Trim('/');
             }
 
-            string smart = NetManager.GetBindInterface(remoteAddr, out var port);
+            string smart = NetManager.GetBindAddress(remoteAddr, out var port);
             return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
 
         /// <inheritdoc/>
         public string GetSmartApiUrl(HttpRequest request)
         {
-            // Return the host in the HTTP request as the API url
+            // Return the host in the HTTP request as the API URL if not configured otherwise
             if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
             {
                 int? requestPort = request.Host.Port;
-                if ((requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase)) || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
+                if (requestPort is null
+                    || (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
+                    || (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
                 {
                     requestPort = -1;
                 }
@@ -1027,15 +932,15 @@ namespace Emby.Server.Implementations
                 return PublishedServerUrl.Trim('/');
             }
 
-            string smart = NetManager.GetBindInterface(hostname, out var port);
+            string smart = NetManager.GetBindAddress(hostname, out var port);
             return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
 
         /// <inheritdoc/>
-        public string GetApiUrlForLocalAccess(IPObject hostname = null, bool allowHttps = true)
+        public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
         {
             // With an empty source, the port will be null
-            var smart = NetManager.GetBindInterface(hostname ?? IPHost.None, out _);
+            var smart = NetManager.GetBindAddress(ipAddress, out _, false);
             var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
             int? port = !allowHttps ? HttpPort : null;
             return GetLocalApiUrl(smart, scheme, port);
@@ -1063,30 +968,6 @@ namespace Emby.Server.Implementations
             }.ToString().TrimEnd('/');
         }
 
-        /// <inheritdoc />
-        public async Task Shutdown()
-        {
-            if (IsShuttingDown)
-            {
-                return;
-            }
-
-            IsShuttingDown = true;
-
-            try
-            {
-                await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
-            }
-            catch (Exception ex)
-            {
-                Logger.LogError(ex, "Error sending server shutdown notification");
-            }
-
-            ShutdownInternal();
-        }
-
-        protected abstract void ShutdownInternal();
-
         public IEnumerable<Assembly> GetApiPluginAssemblies()
         {
             var assemblies = _allConcreteTypes
@@ -1150,52 +1031,5 @@ namespace Emby.Server.Implementations
 
             _disposed = true;
         }
-
-        public async ValueTask DisposeAsync()
-        {
-            await DisposeAsyncCore().ConfigureAwait(false);
-            Dispose(false);
-            GC.SuppressFinalize(this);
-        }
-
-        /// <summary>
-        /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
-        /// </summary>
-        /// <returns>A ValueTask.</returns>
-        protected virtual async ValueTask DisposeAsyncCore()
-        {
-            var type = GetType();
-
-            Logger.LogInformation("Disposing {Type}", type.Name);
-
-            foreach (var (part, _) in _disposableParts)
-            {
-                var partType = part.GetType();
-                if (partType == type)
-                {
-                    continue;
-                }
-
-                Logger.LogInformation("Disposing {Type}", partType.Name);
-
-                try
-                {
-                    part.Dispose();
-                }
-                catch (Exception ex)
-                {
-                    Logger.LogError(ex, "Error disposing {Type}", partType.Name);
-                }
-            }
-
-            if (_sessionManager != null)
-            {
-                // used for closing websockets
-                foreach (var session in _sessionManager.Sessions)
-                {
-                    await session.DisposeAsync().ConfigureAwait(false);
-                }
-            }
-        }
     }
 }

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

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

+ 6 - 5
Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs

@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Serialization;
 using Microsoft.Extensions.Logging;
 
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
         /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
         /// </summary>
         /// <param name="applicationPaths">The application paths.</param>
-        /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
+        /// <param name="loggerFactory">The logger factory.</param>
         /// <param name="xmlSerializer">The XML serializer.</param>
-        /// <param name="fileSystem">The file system.</param>
-        public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
-            : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+        public ServerConfigurationManager(
+            IApplicationPaths applicationPaths,
+            ILoggerFactory loggerFactory,
+            IXmlSerializer xmlSerializer)
+            : base(applicationPaths, loggerFactory, xmlSerializer)
         {
             UpdateMetadataPath();
         }

+ 27 - 114
Emby.Server.Implementations/Data/BaseSqliteRepository.cs

@@ -5,8 +5,8 @@
 using System;
 using System.Collections.Generic;
 using Jellyfin.Extensions;
+using Microsoft.Data.Sqlite;
 using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
 
 namespace Emby.Server.Implementations.Data
 {
@@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
         /// <value>The logger.</value>
         protected ILogger<BaseSqliteRepository> Logger { get; }
 
-        /// <summary>
-        /// Gets the default connection flags.
-        /// </summary>
-        /// <value>The default connection flags.</value>
-        protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
-
-        /// <summary>
-        /// Gets the transaction mode.
-        /// </summary>
-        /// <value>The transaction mode.</value>>
-        protected TransactionMode TransactionMode => TransactionMode.Deferred;
-
-        /// <summary>
-        /// Gets the transaction mode for read-only operations.
-        /// </summary>
-        /// <value>The transaction mode.</value>
-        protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
-
         /// <summary>
         /// Gets the cache size.
         /// </summary>
@@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
         /// <see cref="SynchronousMode"/>
         protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
 
-        /// <summary>
-        /// Gets or sets the write lock.
-        /// </summary>
-        /// <value>The write lock.</value>
-        protected ConnectionPool WriteConnections { get; set; }
-
-        /// <summary>
-        /// Gets or sets the write connection.
-        /// </summary>
-        /// <value>The write connection.</value>
-        protected ConnectionPool ReadConnections { get; set; }
-
         public virtual void Initialize()
         {
-            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())
             {
@@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
             }
         }
 
-        protected ManagedConnection GetConnection(bool readOnly = false)
-            => readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
-
-        protected SQLiteDatabaseConnection CreateWriteConnection()
-        {
-            var writeConnection = SQLite3.Open(
-                DbFilePath,
-                DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
-                null);
-
-            if (CacheSize.HasValue)
-            {
-                writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
-            }
-
-            if (!string.IsNullOrWhiteSpace(LockingMode))
-            {
-                writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
-            }
-
-            if (!string.IsNullOrWhiteSpace(JournalMode))
-            {
-                writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
-            }
-
-            if (JournalSizeLimit.HasValue)
-            {
-                writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
-            }
-
-            if (Synchronous.HasValue)
-            {
-                writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
-            }
-
-            if (PageSize.HasValue)
-            {
-                writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
-            }
-
-            writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
-
-            return writeConnection;
-        }
-
-        protected SQLiteDatabaseConnection CreateReadConnection()
+        protected SqliteConnection GetConnection()
         {
-            var connection = SQLite3.Open(
-                DbFilePath,
-                DefaultConnectionFlags | ConnectionFlags.ReadOnly,
-                null);
+            var connection = new SqliteConnection($"Filename={DbFilePath}");
+            connection.Open();
 
             if (CacheSize.HasValue)
             {
@@ -208,39 +128,38 @@ namespace Emby.Server.Implementations.Data
                 connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
             }
 
+            if (PageSize.HasValue)
+            {
+                connection.Execute("PRAGMA page_size=" + PageSize.Value);
+            }
+
             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);
+        public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
+        {
+            var command = connection.CreateCommand();
+            command.CommandText = sql;
+            return command;
+        }
 
-        protected bool TableExists(ManagedConnection connection, string name)
+        protected bool TableExists(SqliteConnection connection, string name)
         {
-            return connection.RunInTransaction(
-                db =>
+            using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
+            foreach (var row in statement.ExecuteQuery())
+            {
+                if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
                 {
-                    using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
-                    {
-                        foreach (var row in statement.ExecuteQuery())
-                        {
-                            if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
-                            {
-                                return true;
-                            }
-                        }
-                    }
-
-                    return false;
-                },
-                ReadTransactionMode);
+                    return true;
+                }
+            }
+
+            return false;
         }
 
-        protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
+        protected List<string> GetColumnNames(SqliteConnection connection, string table)
         {
             var columnNames = new List<string>();
 
@@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
             return columnNames;
         }
 
-        protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
+        protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
         {
             if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
             {
@@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
                 return;
             }
 
-            if (dispose)
-            {
-                WriteConnections.Dispose();
-                ReadConnections.Dispose();
-            }
-
             _disposed = true;
         }
     }

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

@@ -1,79 +0,0 @@
-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;
-    }
-}

+ 0 - 81
Emby.Server.Implementations/Data/ManagedConnection.cs

@@ -1,81 +0,0 @@
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using SQLitePCL.pretty;
-
-namespace Emby.Server.Implementations.Data
-{
-    public sealed class ManagedConnection : IDisposable
-    {
-        private readonly ConnectionPool _pool;
-
-        private SQLiteDatabaseConnection _db;
-
-        private bool _disposed = false;
-
-        public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
-        {
-            _db = db;
-            _pool = pool;
-        }
-
-        public IStatement PrepareStatement(string sql)
-        {
-            return _db.PrepareStatement(sql);
-        }
-
-        public IEnumerable<IStatement> PrepareAll(string sql)
-        {
-            return _db.PrepareAll(sql);
-        }
-
-        public void ExecuteAll(string sql)
-        {
-            _db.ExecuteAll(sql);
-        }
-
-        public void Execute(string sql, params object[] values)
-        {
-            _db.Execute(sql, values);
-        }
-
-        public void RunQueries(string[] sql)
-        {
-            _db.RunQueries(sql);
-        }
-
-        public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
-        {
-            _db.RunInTransaction(action, mode);
-        }
-
-        public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
-        {
-            return _db.RunInTransaction(action, mode);
-        }
-
-        public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
-        {
-            return _db.Query(sql);
-        }
-
-        public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
-        {
-            return _db.Query(sql, values);
-        }
-
-        public void Dispose()
-        {
-            if (_disposed)
-            {
-                return;
-            }
-
-            _pool.Return(_db);
-
-            _db = null!; // Don't dispose it
-            _disposed = true;
-        }
-    }
-}

+ 70 - 263
Emby.Server.Implementations/Data/SqliteExtensions.cs

@@ -1,11 +1,10 @@
-#nullable disable
 #pragma warning disable CS1591
 
 using System;
 using System.Collections.Generic;
-using System.Diagnostics;
+using System.Data;
 using System.Globalization;
-using SQLitePCL.pretty;
+using Microsoft.Data.Sqlite;
 
 namespace Emby.Server.Implementations.Data
 {
@@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
             "yy-MM-dd"
         };
 
-        public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
+        public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
         {
-            ArgumentNullException.ThrowIfNull(queries);
+            if (sqliteConnection.State != ConnectionState.Open)
+            {
+                sqliteConnection.Open();
+            }
 
-            connection.RunInTransaction(conn =>
+            using var command = sqliteConnection.CreateCommand();
+            command.CommandText = commandText;
+            using (var reader = command.ExecuteReader())
             {
-                conn.ExecuteAll(string.Join(';', queries));
-            });
+                while (reader.Read())
+                {
+                    yield return reader;
+                }
+            }
         }
 
-        public static Guid ReadGuidFromBlob(this ResultSetValue result)
+        public static void Execute(this SqliteConnection sqliteConnection, string commandText)
         {
-            return new Guid(result.ToBlob());
+            using var command = sqliteConnection.CreateCommand();
+            command.CommandText = commandText;
+            command.ExecuteNonQuery();
         }
 
         public static string ToDateTimeParamValue(this DateTime dateValue)
@@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
         private static string GetDateTimeKindFormat(DateTimeKind kind)
             => (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
 
-        public static DateTime ReadDateTime(this ResultSetValue result)
-        {
-            var dateText = result.ToString();
-
-            return DateTime.ParseExact(
-                dateText,
-                _datetimeFormats,
-                DateTimeFormatInfo.InvariantInfo,
-                DateTimeStyles.AdjustToUniversal);
-        }
-
-        public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
+        public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            var dateText = item.ToString();
+            var dateText = reader.GetString(index);
 
             if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
             {
@@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
             return false;
         }
 
-        public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
+        public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ReadGuidFromBlob();
+            result = reader.GetGuid(index);
             return true;
         }
 
-        public static bool IsDbNull(this ResultSetValue result)
+        public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
         {
-            return result.SQLiteType == SQLiteType.Null;
-        }
-
-        public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
-        {
-            return result[index].ToString();
-        }
+            result = string.Empty;
 
-        public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
-        {
-            result = null;
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 return false;
             }
 
-            result = item.ToString();
+            result = reader.GetString(index);
             return true;
         }
 
-        public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
-        {
-            return result[index].ToBool();
-        }
-
-        public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
+        public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ToBool();
+            result = reader.GetBoolean(index);
             return true;
         }
 
-        public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
+        public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ToInt();
+            result = reader.GetInt32(index);
             return true;
         }
 
-        public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
+        public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
         {
-            return result[index].ToInt64();
-        }
-
-        public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
-        {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ToInt64();
+            result = reader.GetInt64(index);
             return true;
         }
 
-        public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
+        public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ToFloat();
+            result = reader.GetFloat(index);
             return true;
         }
 
-        public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
+        public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
         {
-            var item = reader[index];
-            if (item.IsDbNull())
+            if (reader.IsDBNull(index))
             {
                 result = default;
                 return false;
             }
 
-            result = item.ToDouble();
+            result = reader.GetDouble(index);
             return true;
         }
 
-        public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
+        public static void TryBind(this SqliteCommand statement, string name, Guid value)
         {
-            return result[index].ReadGuidFromBlob();
+            statement.TryBind(name, value, true);
         }
 
-        [Conditional("DEBUG")]
-        private static void CheckName(string name)
+        public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
         {
-            throw new ArgumentException("Invalid param name: " + name, nameof(name));
-        }
-
-        public static void TryBind(this IStatement statement, string name, double value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
+            var preparedValue = value ?? DBNull.Value;
+            if (statement.Parameters.Contains(name))
             {
-                bindParam.Bind(value);
+                statement.Parameters[name].Value = preparedValue;
             }
             else
             {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, string value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                if (value is null)
+                // Blobs aren't always detected automatically
+                if (isBlob)
                 {
-                    bindParam.BindNull();
+                    statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
                 }
                 else
                 {
-                    bindParam.Bind(value);
+                    statement.Parameters.AddWithValue(name, preparedValue);
                 }
             }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, bool value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, float value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, int value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, Guid value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                Span<byte> byteValue = stackalloc byte[16];
-                value.TryWriteBytes(byteValue);
-                bindParam.Bind(byteValue);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, DateTime value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value.ToDateTimeParamValue());
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, long value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.Bind(value);
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBindNull(this IStatement statement, string name)
-        {
-            if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
-            {
-                bindParam.BindNull();
-            }
-            else
-            {
-                CheckName(name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, DateTime? value)
-        {
-            if (value.HasValue)
-            {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, Guid? value)
-        {
-            if (value.HasValue)
-            {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
-            }
-        }
-
-        public static void TryBind(this IStatement statement, string name, double? value)
-        {
-            if (value.HasValue)
-            {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
-            }
         }
 
-        public static void TryBind(this IStatement statement, string name, int? value)
+        public static void TryBindNull(this SqliteCommand statement, string name)
         {
-            if (value.HasValue)
-            {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
-            }
+            statement.TryBind(name, DBNull.Value);
         }
 
-        public static void TryBind(this IStatement statement, string name, float? value)
+        public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
         {
-            if (value.HasValue)
+            using (var reader = command.ExecuteReader())
             {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
+                while (reader.Read())
+                {
+                    yield return reader;
+                }
             }
         }
 
-        public static void TryBind(this IStatement statement, string name, bool? value)
+        public static int SelectScalarInt(this SqliteCommand command)
         {
-            if (value.HasValue)
-            {
-                TryBind(statement, name, value.Value);
-            }
-            else
-            {
-                TryBindNull(statement, name);
-            }
+            var result = command.ExecuteScalar();
+            // Can't be null since the method is used to retrieve Count
+            return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
         }
 
-        public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
+        public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
         {
-            while (statement.MoveNext())
-            {
-                yield return statement.Current;
-            }
+            var command = sqliteConnection.CreateCommand();
+            command.CommandText = sql;
+            return command;
         }
     }
 }

File diff suppressed because it is too large
+ 358 - 456
Emby.Server.Implementations/Data/SqliteItemRepository.cs


+ 60 - 66
Emby.Server.Implementations/Data/SqliteUserDataRepository.cs

@@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Persistence;
+using Microsoft.Data.Sqlite;
 using Microsoft.Extensions.Logging;
-using SQLitePCL.pretty;
 
 namespace Emby.Server.Implementations.Data
 {
@@ -44,48 +44,48 @@ namespace Emby.Server.Implementations.Data
                 var userDataTableExists = TableExists(connection, "userdata");
 
                 var users = userDatasTableExists ? null : _userManager.Users;
+                using var transaction = connection.BeginTransaction();
+                connection.Execute(string.Join(
+                    ';',
+                    "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
+                    "drop index if exists idx_userdata",
+                    "drop index if exists idx_userdata1",
+                    "drop index if exists idx_userdata2",
+                    "drop index if exists userdataindex1",
+                    "drop index if exists userdataindex",
+                    "drop index if exists userdataindex3",
+                    "drop index if exists userdataindex4",
+                    "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
+                    "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
+                    "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
+                    "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
+
+                if (!userDataTableExists)
+                {
+                    transaction.Commit();
+                    return;
+                }
 
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        db.ExecuteAll(string.Join(';', new[]
-                        {
-                            "create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
-
-                            "drop index if exists idx_userdata",
-                            "drop index if exists idx_userdata1",
-                            "drop index if exists idx_userdata2",
-                            "drop index if exists userdataindex1",
-                            "drop index if exists userdataindex",
-                            "drop index if exists userdataindex3",
-                            "drop index if exists userdataindex4",
-                            "create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
-                            "create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
-                            "create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
-                            "create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
-                        }));
-
-                        if (userDataTableExists)
-                        {
-                            var existingColumnNames = GetColumnNames(db, "userdata");
-
-                            AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
-                            AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
-                            AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
-
-                            if (!userDatasTableExists)
-                            {
-                                ImportUserIds(db, users);
-
-                                db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
-                            }
-                        }
-                    },
-                    TransactionMode);
+                var existingColumnNames = GetColumnNames(connection, "userdata");
+
+                AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
+                AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
+                AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
+
+                if (userDatasTableExists)
+                {
+                    return;
+                }
+
+                ImportUserIds(connection, users);
+
+                connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
+
+                transaction.Commit();
             }
         }
 
-        private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
+        private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
         {
             var userIdsWithUserData = GetAllUserIdsWithUserData(db);
 
@@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
                     statement.TryBind("@UserId", user.Id);
                     statement.TryBind("@InternalUserId", user.InternalId);
 
-                    statement.MoveNext();
-                    statement.Reset();
+                    statement.ExecuteNonQuery();
                 }
             }
         }
 
-        private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
+        private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
         {
             var list = new List<Guid>();
 
@@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
                 {
                     try
                     {
-                        list.Add(row[0].ReadGuidFromBlob());
+                        list.Add(row.GetGuid(0));
                     }
                     catch (Exception ex)
                     {
@@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
             cancellationToken.ThrowIfCancellationRequested();
 
             using (var connection = GetConnection())
+            using (var transaction = connection.BeginTransaction())
             {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        SaveUserData(db, internalUserId, key, userData);
-                    },
-                    TransactionMode);
+                SaveUserData(connection, internalUserId, key, userData);
+                transaction.Commit();
             }
         }
 
-        private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
+        private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
         {
             using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
             {
@@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
                     statement.TryBindNull("@SubtitleStreamIndex");
                 }
 
-                statement.MoveNext();
+                statement.ExecuteNonQuery();
             }
         }
 
@@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
             cancellationToken.ThrowIfCancellationRequested();
 
             using (var connection = GetConnection())
+            using (var transaction = connection.BeginTransaction())
             {
-                connection.RunInTransaction(
-                    db =>
-                    {
-                        foreach (var userItemData in userDataList)
-                        {
-                            SaveUserData(db, internalUserId, userItemData.Key, userItemData);
-                        }
-                    },
-                    TransactionMode);
+                foreach (var userItemData in userDataList)
+                {
+                    SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
+                }
+
+                transaction.Commit();
             }
         }
 
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
 
             ArgumentException.ThrowIfNullOrEmpty(key);
 
-            using (var connection = GetConnection(true))
+            using (var connection = GetConnection())
             {
                 using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
                 {
@@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
         /// </summary>
         /// <param name="reader">The list of result set values.</param>
         /// <returns>The user item data.</returns>
-        private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
+        private UserItemData ReadRow(SqliteDataReader reader)
         {
             var userData = new UserItemData();
 
@@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
                 userData.Rating = rating;
             }
 
-            userData.Played = reader[3].ToBool();
-            userData.PlayCount = reader[4].ToInt();
-            userData.IsFavorite = reader[5].ToBool();
-            userData.PlaybackPositionTicks = reader[6].ToInt64();
+            userData.Played = reader.GetBoolean(3);
+            userData.PlayCount = reader.GetInt32(4);
+            userData.IsFavorite = reader.GetBoolean(5);
+            userData.PlaybackPositionTicks = reader.GetInt64(6);
 
             if (reader.TryReadDateTime(7, out var lastPlayedDate))
             {

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

@@ -907,10 +907,11 @@ namespace Emby.Server.Implementations.Dto
                 dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
             }
 
+            dto.LUFS = item.LUFS;
+
             // Add audio info
             if (item is Audio audio)
             {
-                dto.LUFS = audio.LUFS;
                 dto.Album = audio.Album;
                 if (audio.ExtraType.HasValue)
                 {

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

@@ -24,6 +24,7 @@
   <ItemGroup>
     <PackageReference Include="DiscUtils.Udf" />
     <PackageReference Include="Jellyfin.XmlTv" />
+    <PackageReference Include="Microsoft.Data.Sqlite" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
     <PackageReference Include="Microsoft.Extensions.Caching.Memory" />
     <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
@@ -31,7 +32,6 @@
     <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
     <PackageReference Include="Mono.Nat" />
     <PackageReference Include="prometheus-net.DotNetRuntime" />
-    <PackageReference Include="SQLitePCL.pretty.netstandard" />
     <PackageReference Include="DotNet.Glob" />
   </ItemGroup>
 
@@ -43,8 +43,6 @@
     <TargetFramework>net7.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
-    <NoWarn>AD0001</NoWarn>
   </PropertyGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

+ 2 - 2
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -57,7 +57,7 @@ namespace Emby.Server.Implementations.EntryPoints
 
             return new StringBuilder(32)
                 .Append(config.EnableUPnP).Append(Separator)
-                .Append(config.PublicPort).Append(Separator)
+                .Append(config.PublicHttpPort).Append(Separator)
                 .Append(config.PublicHttpsPort).Append(Separator)
                 .Append(_appHost.HttpPort).Append(Separator)
                 .Append(_appHost.HttpsPort).Append(Separator)
@@ -146,7 +146,7 @@ namespace Emby.Server.Implementations.EntryPoints
         private IEnumerable<Task> CreatePortMaps(INatDevice device)
         {
             var config = _config.GetNetworkConfiguration();
-            yield return CreatePortMap(device, _appHost.HttpPort, config.PublicPort);
+            yield return CreatePortMap(device, _appHost.HttpPort, config.PublicHttpPort);
 
             if (_appHost.ListenWithHttps)
             {

+ 58 - 10
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -1,10 +1,15 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
 using System.Net.Sockets;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Server.Implementations.Udp;
 using Jellyfin.Networking.Configuration;
+using Jellyfin.Networking.Extensions;
 using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Plugins;
 using Microsoft.Extensions.Configuration;
@@ -13,7 +18,7 @@ using Microsoft.Extensions.Logging;
 namespace Emby.Server.Implementations.EntryPoints
 {
     /// <summary>
-    /// Class UdpServerEntryPoint.
+    /// Class responsible for registering all UDP broadcast endpoints and their handlers.
     /// </summary>
     public sealed class UdpServerEntryPoint : IServerEntryPoint
     {
@@ -29,13 +34,14 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IServerApplicationHost _appHost;
         private readonly IConfiguration _config;
         private readonly IConfigurationManager _configurationManager;
+        private readonly INetworkManager _networkManager;
 
         /// <summary>
         /// The UDP server.
         /// </summary>
-        private UdpServer? _udpServer;
-        private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
-        private bool _disposed = false;
+        private readonly List<UdpServer> _udpServers;
+        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+        private bool _disposed;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@@ -44,16 +50,20 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param>
         /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
         /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
+        /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
         public UdpServerEntryPoint(
             ILogger<UdpServerEntryPoint> logger,
             IServerApplicationHost appHost,
             IConfiguration configuration,
-            IConfigurationManager configurationManager)
+            IConfigurationManager configurationManager,
+            INetworkManager networkManager)
         {
             _logger = logger;
             _appHost = appHost;
             _config = configuration;
             _configurationManager = configurationManager;
+            _networkManager = networkManager;
+            _udpServers = new List<UdpServer>();
         }
 
         /// <inheritdoc />
@@ -68,8 +78,43 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber);
-                _udpServer.Start(_cancellationTokenSource.Token);
+                // Linux needs to bind to the broadcast addresses to get broadcast traffic
+                // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
+                if (OperatingSystem.IsLinux())
+                {
+                    // Add global broadcast listener
+                    var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
+                    server.Start(_cancellationTokenSource.Token);
+                    _udpServers.Add(server);
+
+                    // Add bind address specific broadcast listeners
+                    // IPv6 is currently unsupported
+                    var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+                    foreach (var intf in validInterfaces)
+                    {
+                        var broadcastAddress = NetworkExtensions.GetBroadcastAddress(intf.Subnet);
+                        _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber);
+
+                        server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber);
+                        server.Start(_cancellationTokenSource.Token);
+                        _udpServers.Add(server);
+                    }
+                }
+                else
+                {
+                    // Add bind address specific broadcast listeners
+                    // IPv6 is currently unsupported
+                    var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
+                    foreach (var intf in validInterfaces)
+                    {
+                        var intfAddress = intf.Address;
+                        _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
+
+                        var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
+                        server.Start(_cancellationTokenSource.Token);
+                        _udpServers.Add(server);
+                    }
+                }
             }
             catch (SocketException ex)
             {
@@ -83,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             if (_disposed)
             {
-                throw new ObjectDisposedException(this.GetType().Name);
+                throw new ObjectDisposedException(GetType().Name);
             }
         }
 
@@ -97,9 +142,12 @@ namespace Emby.Server.Implementations.EntryPoints
 
             _cancellationTokenSource.Cancel();
             _cancellationTokenSource.Dispose();
-            _udpServer?.Dispose();
-            _udpServer = null;
+            foreach (var server in _udpServers)
+            {
+                server.Dispose();
+            }
 
+            _udpServers.Clear();
             _disposed = true;
         }
     }

+ 26 - 33
Emby.Server.Implementations/HttpServer/WebSocketConnection.cs

@@ -9,7 +9,8 @@ using System.Threading;
 using System.Threading.Tasks;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Controller.Net;
-using MediaBrowser.Model.Net;
+using MediaBrowser.Controller.Net.WebSocketMessages;
+using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 using MediaBrowser.Model.Session;
 using Microsoft.Extensions.Logging;
 
@@ -42,14 +43,17 @@ namespace Emby.Server.Implementations.HttpServer
         /// </summary>
         /// <param name="logger">The logger.</param>
         /// <param name="socket">The socket.</param>
+        /// <param name="authorizationInfo">The authorization information.</param>
         /// <param name="remoteEndPoint">The remote end point.</param>
         public WebSocketConnection(
             ILogger<WebSocketConnection> logger,
             WebSocket socket,
+            AuthorizationInfo authorizationInfo,
             IPAddress? remoteEndPoint)
         {
             _logger = logger;
             _socket = socket;
+            AuthorizationInfo = authorizationInfo;
             RemoteEndPoint = remoteEndPoint;
 
             _jsonOptions = JsonDefaults.Options;
@@ -59,47 +63,40 @@ namespace Emby.Server.Implementations.HttpServer
         /// <inheritdoc />
         public event EventHandler<EventArgs>? Closed;
 
-        /// <summary>
-        /// Gets the remote end point.
-        /// </summary>
+        /// <inheritdoc />
+        public AuthorizationInfo AuthorizationInfo { get; }
+
+        /// <inheritdoc />
         public IPAddress? RemoteEndPoint { get; }
 
-        /// <summary>
-        /// Gets or sets the receive action.
-        /// </summary>
-        /// <value>The receive action.</value>
+        /// <inheritdoc />
         public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
 
-        /// <summary>
-        /// Gets the last activity date.
-        /// </summary>
-        /// <value>The last activity date.</value>
+        /// <inheritdoc />
         public DateTime LastActivityDate { get; private set; }
 
         /// <inheritdoc />
         public DateTime LastKeepAliveDate { get; set; }
 
-        /// <summary>
-        /// Gets the state.
-        /// </summary>
-        /// <value>The state.</value>
+        /// <inheritdoc />
         public WebSocketState State => _socket.State;
 
-        /// <summary>
-        /// Sends a message asynchronously.
-        /// </summary>
-        /// <typeparam name="T">The type of the message.</typeparam>
-        /// <param name="message">The message.</param>
-        /// <param name="cancellationToken">The cancellation token.</param>
-        /// <returns>Task.</returns>
-        public Task SendAsync<T>(WebSocketMessage<T> message, CancellationToken cancellationToken)
+        /// <inheritdoc />
+        public Task SendAsync(OutboundWebSocketMessage message, CancellationToken cancellationToken)
+        {
+            var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
+            return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
+        }
+
+        /// <inheritdoc />
+        public Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken)
         {
             var json = JsonSerializer.SerializeToUtf8Bytes(message, _jsonOptions);
             return _socket.SendAsync(json, WebSocketMessageType.Text, true, cancellationToken);
         }
 
         /// <inheritdoc />
-        public async Task ProcessAsync(CancellationToken cancellationToken = default)
+        public async Task ReceiveAsync(CancellationToken cancellationToken = default)
         {
             var pipe = new Pipe();
             var writer = pipe.Writer;
@@ -171,7 +168,7 @@ namespace Emby.Server.Implementations.HttpServer
                 return;
             }
 
-            WebSocketMessage<object>? stub;
+            InboundWebSocketMessage<object>? stub;
             long bytesConsumed;
             try
             {
@@ -212,10 +209,10 @@ namespace Emby.Server.Implementations.HttpServer
             }
         }
 
-        internal WebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
+        internal InboundWebSocketMessage<object>? DeserializeWebSocketMessage(ReadOnlySequence<byte> bytes, out long bytesConsumed)
         {
             var jsonReader = new Utf8JsonReader(bytes);
-            var ret = JsonSerializer.Deserialize<WebSocketMessage<object>>(ref jsonReader, _jsonOptions);
+            var ret = JsonSerializer.Deserialize<InboundWebSocketMessage<object>>(ref jsonReader, _jsonOptions);
             bytesConsumed = jsonReader.BytesConsumed;
             return ret;
         }
@@ -224,11 +221,7 @@ namespace Emby.Server.Implementations.HttpServer
         {
             LastKeepAliveDate = DateTime.UtcNow;
             return SendAsync(
-                new WebSocketMessage<string>
-                {
-                    MessageId = Guid.NewGuid(),
-                    MessageType = SessionMessageType.KeepAlive
-                },
+                new OutboundKeepAliveMessage(),
                 CancellationToken.None);
         }
 

+ 3 - 2
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -51,7 +51,8 @@ namespace Emby.Server.Implementations.HttpServer
                 using var connection = new WebSocketConnection(
                     _loggerFactory.CreateLogger<WebSocketConnection>(),
                     webSocket,
-                    context.GetNormalizedRemoteIp())
+                    authorizationInfo,
+                    context.GetNormalizedRemoteIP())
                 {
                     OnReceive = ProcessWebSocketMessageReceived
                 };
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
 
                 await Task.WhenAll(tasks).ConfigureAwait(false);
 
-                await connection.ProcessAsync().ConfigureAwait(false);
+                await connection.ReceiveAsync().ConfigureAwait(false);
                 _logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
             }
             catch (Exception ex) // Otherwise ASP.Net will ignore the exception

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

@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        public void ResetPath(string path, string affectedFile)
+        public void ResetPath(string path, string? affectedFile)
         {
             lock (_timerLock)
             {
@@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
                 {
                     item.ChangedExternally();
                 }
-                catch (IOException ex)
-                {
-                    // For now swallow and log.
-                    // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
-                    // Should we remove it from it's parent?
-                    _logger.LogError(ex, "Error refreshing {Name}", item.Name);
-                }
                 catch (Exception ex)
                 {
                     _logger.LogError(ex, "Error refreshing {Name}", item.Name);
@@ -217,7 +210,6 @@ namespace Emby.Server.Implementations.IO
 
             DisposeTimer();
             _disposed = true;
-            GC.SuppressFinalize(this);
         }
     }
 }

+ 35 - 33
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
+        private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
         {
             if (e.Parent is AggregateFolder)
             {
@@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         /// <param name="sender">The source of the event.</param>
         /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
-        private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
         {
             if (e.Parent is AggregateFolder)
             {
@@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
         /// <param name="path">The path.</param>
         /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
         /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
-        private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
+        private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
         {
-            ArgumentException.ThrowIfNullOrEmpty(path);
+            if (path.IsEmpty)
+            {
+                throw new ArgumentException("Path can't be empty", nameof(path));
+            }
 
             path = path.TrimEnd(Path.DirectorySeparatorChar);
 
-            return lst.Any(str =>
+            foreach (var str in lst)
             {
                 // this should be a little quicker than examining each actual parent folder...
-                var compare = str.TrimEnd(Path.DirectorySeparatorChar);
+                var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
 
-                return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
-            });
+                if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
+                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
+                {
+                    return true;
+                }
+            }
+
+            return false;
         }
 
         /// <summary>
@@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
         {
             ArgumentException.ThrowIfNullOrEmpty(path);
 
-            var monitorPath = !IgnorePatterns.ShouldIgnore(path);
+            if (IgnorePatterns.ShouldIgnore(path))
+            {
+                return;
+            }
 
             // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
-            if (_tempIgnoredPaths.Keys.Any(i =>
+            foreach (var i in _tempIgnoredPaths.Keys)
             {
-                if (_fileSystem.AreEqual(i, path))
-                {
-                    _logger.LogDebug("Ignoring change to {Path}", path);
-                    return true;
-                }
-
-                if (_fileSystem.ContainsSubPath(i, path))
+                if (_fileSystem.AreEqual(i, path)
+                    || _fileSystem.ContainsSubPath(i, path))
                 {
                     _logger.LogDebug("Ignoring change to {Path}", path);
-                    return true;
+                    return;
                 }
 
                 // Go up a level
@@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
                 if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
                 {
                     _logger.LogDebug("Ignoring change to {Path}", path);
-                    return true;
+                    return;
                 }
-
-                return false;
-            }))
-            {
-                monitorPath = false;
             }
 
-            if (monitorPath)
-            {
-                // Avoid implicitly captured closure
-                CreateRefresher(path);
-            }
+            CreateRefresher(path);
         }
 
         private void CreateRefresher(string path)
@@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
                     }
 
                     // They are siblings. Rebase the refresher to the parent folder.
-                    if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
+                    if (parentPath is not null
+                        && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
                     {
                         refresher.ResetPath(parentPath, path);
                         return;
@@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
-        private void OnNewRefresherCompleted(object sender, EventArgs e)
+        private void OnNewRefresherCompleted(object? sender, EventArgs e)
         {
+            if (sender is null)
+            {
+                return;
+            }
+
             var refresher = (FileRefresher)sender;
             DisposeRefresher(refresher);
         }

+ 25 - 33
Emby.Server.Implementations/IO/ManagedFileSystem.cs

@@ -15,29 +15,34 @@ namespace Emby.Server.Implementations.IO
     /// </summary>
     public class ManagedFileSystem : IFileSystem
     {
-        private readonly ILogger<ManagedFileSystem> _logger;
+        private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
+        private static readonly char[] _invalidPathCharacters =
+        {
+            '\"', '<', '>', '|', '\0',
+            (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10,
+            (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20,
+            (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30,
+            (char)31, ':', '*', '?', '\\', '/'
+        };
 
-        private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
+        private readonly ILogger<ManagedFileSystem> _logger;
+        private readonly List<IShortcutHandler> _shortcutHandlers;
         private readonly string _tempPath;
-        private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
         /// </summary>
         /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
         /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
+        /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
         public ManagedFileSystem(
             ILogger<ManagedFileSystem> logger,
-            IApplicationPaths applicationPaths)
+            IApplicationPaths applicationPaths,
+            IEnumerable<IShortcutHandler> shortcutHandlers)
         {
             _logger = logger;
             _tempPath = applicationPaths.TempDirectory;
-        }
-
-        /// <inheritdoc />
-        public virtual void AddShortcutHandler(IShortcutHandler handler)
-        {
-            _shortcutHandlers.Add(handler);
+            _shortcutHandlers = shortcutHandlers.ToList();
         }
 
         /// <summary>
@@ -86,7 +91,7 @@ namespace Emby.Server.Implementations.IO
             }
 
             // unc path
-            if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+            if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
             {
                 return filePath;
             }
@@ -98,15 +103,17 @@ namespace Emby.Server.Implementations.IO
                 return filePath;
             }
 
+            var filePathSpan = filePath.AsSpan();
+
             // relative path
             if (firstChar == '\\')
             {
-                filePath = filePath.Substring(1);
+                filePathSpan = filePathSpan.Slice(1);
             }
 
             try
             {
-                return Path.GetFullPath(Path.Combine(folderPath, filePath));
+                return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
             }
             catch (ArgumentException)
             {
@@ -275,8 +282,7 @@ namespace Emby.Server.Implementations.IO
         /// <exception cref="ArgumentNullException">The filename is null.</exception>
         public string GetValidFilename(string filename)
         {
-            var invalid = Path.GetInvalidFileNameChars();
-            var first = filename.IndexOfAny(invalid);
+            var first = filename.IndexOfAny(_invalidPathCharacters);
             if (first == -1)
             {
                 // Fast path for clean strings
@@ -285,7 +291,7 @@ namespace Emby.Server.Implementations.IO
 
             return string.Create(
                 filename.Length,
-                (filename, invalid, first),
+                (filename, _invalidPathCharacters, first),
                 (chars, state) =>
                 {
                     state.filename.AsSpan().CopyTo(chars);
@@ -293,7 +299,7 @@ namespace Emby.Server.Implementations.IO
                     chars[state.first++] = ' ';
 
                     var len = chars.Length;
-                    foreach (var c in state.invalid)
+                    foreach (var c in state._invalidPathCharacters)
                     {
                         for (int i = state.first; i < len; i++)
                         {
@@ -478,25 +484,11 @@ namespace Emby.Server.Implementations.IO
                 _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
         }
 
-        /// <inheritdoc />
-        public virtual string NormalizePath(string path)
-        {
-            ArgumentException.ThrowIfNullOrEmpty(path);
-
-            if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
-            {
-                return path;
-            }
-
-            return Path.TrimEndingDirectorySeparator(path);
-        }
-
         /// <inheritdoc />
         public virtual bool AreEqual(string path1, string path2)
         {
-            return string.Equals(
-                NormalizePath(path1),
-                NormalizePath(path2),
+            return Path.TrimEndingDirectorySeparator(path1).Equals(
+                Path.TrimEndingDirectorySeparator(path2),
                 _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
         }
 

+ 2 - 9
Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs

@@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
 {
     public class MbLinkShortcutHandler : IShortcutHandler
     {
-        private readonly IFileSystem _fileSystem;
-
-        public MbLinkShortcutHandler(IFileSystem fileSystem)
-        {
-            _fileSystem = fileSystem;
-        }
-
         public string Extension => ".mblink";
 
         public string? Resolve(string shortcutPath)
         {
             ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
 
-            if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
+            if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
             {
                 var path = File.ReadAllText(shortcutPath);
 
-                return _fileSystem.NormalizePath(path);
+                return Path.TrimEndingDirectorySeparator(path);
             }
 
             return null;

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

@@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images
             return _libraryManager.GetItemList(new InternalItemsQuery
             {
                 Parent = item,
+                Recursive = true,
                 DtoOptions = new DtoOptions(true),
                 ImageTypes = new ImageType[] { ImageType.Primary },
                 OrderBy = new (string, SortOrder)[]

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

@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
             // bts sync files
             "**/*.bts",
             "**/*.sync",
+
+            // zfs
+            "**/.zfs/**",
+            "**/.zfs"
         };
 
         private static readonly GlobOptions _globOptions = new GlobOptions

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

@@ -3,6 +3,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -45,7 +46,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.Library;
 using MediaBrowser.Model.Querying;
 using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Episode = MediaBrowser.Controller.Entities.TV.Episode;
 using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library
         private const string ShortcutFileExtension = ".mblink";
 
         private readonly ILogger<LibraryManager> _logger;
-        private readonly IMemoryCache _memoryCache;
+        private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
         private readonly ITaskManager _taskManager;
         private readonly IUserManager _userManager;
         private readonly IUserDataManager _userDataRepository;
@@ -111,7 +111,6 @@ namespace Emby.Server.Implementations.Library
         /// <param name="mediaEncoder">The media encoder.</param>
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="imageProcessor">The image processor.</param>
-        /// <param name="memoryCache">The memory cache.</param>
         /// <param name="namingOptions">The naming options.</param>
         /// <param name="directoryService">The directory service.</param>
         public LibraryManager(
@@ -128,7 +127,6 @@ namespace Emby.Server.Implementations.Library
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepository,
             IImageProcessor imageProcessor,
-            IMemoryCache memoryCache,
             NamingOptions namingOptions,
             IDirectoryService directoryService)
         {
@@ -145,7 +143,7 @@ namespace Emby.Server.Implementations.Library
             _mediaEncoder = mediaEncoder;
             _itemRepository = itemRepository;
             _imageProcessor = imageProcessor;
-            _memoryCache = memoryCache;
+            _cache = new ConcurrentDictionary<Guid, BaseItem>();
             _namingOptions = namingOptions;
 
             _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
@@ -300,7 +298,7 @@ namespace Emby.Server.Implementations.Library
                 }
             }
 
-            _memoryCache.Set(item.Id, item);
+            _cache[item.Id] = item;
         }
 
         public void DeleteItem(BaseItem item, DeleteOptions options)
@@ -359,7 +357,7 @@ namespace Emby.Server.Implementations.Library
 
             var children = item.IsFolder
                 ? ((Folder)item).GetRecursiveChildren(false)
-                : Enumerable.Empty<BaseItem>();
+                : Array.Empty<BaseItem>();
 
             foreach (var metadataPath in GetMetadataPaths(item, children))
             {
@@ -441,7 +439,7 @@ namespace Emby.Server.Implementations.Library
                 _itemRepository.DeleteItem(child.Id);
             }
 
-            _memoryCache.Remove(item.Id);
+            _cache.TryRemove(item.Id, out _);
 
             ReportItemRemoved(item, parent);
         }
@@ -609,7 +607,7 @@ namespace Emby.Server.Implementations.Library
             var originalList = paths.ToList();
 
             var list = originalList.Where(i => i.IsDirectory)
-                .Select(i => _fileSystem.NormalizePath(i.FullName))
+                .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .ToList();
 
@@ -840,19 +838,12 @@ namespace Emby.Server.Implementations.Library
         {
             var path = Person.GetPath(name);
             var id = GetItemByNameId<Person>(path);
-            if (GetItemById(id) is not Person item)
+            if (GetItemById(id) is Person item)
             {
-                item = new Person
-                {
-                    Name = name,
-                    Id = id,
-                    DateCreated = DateTime.UtcNow,
-                    DateModified = DateTime.UtcNow,
-                    Path = path
-                };
+                return item;
             }
 
-            return item;
+            return null;
         }
 
         /// <summary>
@@ -1163,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
                 Name = Path.GetFileName(dir),
 
                 Locations = _fileSystem.GetFilePaths(dir, false)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                     .Select(i =>
                     {
                         try
@@ -1233,7 +1224,7 @@ namespace Emby.Server.Implementations.Library
                 throw new ArgumentException("Guid can't be empty", nameof(id));
             }
 
-            if (_memoryCache.TryGetValue(id, out BaseItem item))
+            if (_cache.TryGetValue(id, out BaseItem item))
             {
                 return item;
             }
@@ -2069,7 +2060,9 @@ namespace Emby.Server.Implementations.Library
                     .Find(folder => folder is CollectionFolder) as CollectionFolder;
             }
 
-            return collectionFolder is null ? new LibraryOptions() : collectionFolder.GetLibraryOptions();
+            return collectionFolder is null
+                ? new LibraryOptions()
+                : collectionFolder.GetLibraryOptions();
         }
 
         public string GetContentType(BaseItem item)
@@ -2857,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
 
-                    File.WriteAllBytes(path, Array.Empty<byte>());
+                    await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
                 }
 
                 CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@@ -2899,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
                 var saveEntity = false;
                 var personEntity = GetPerson(person.Name);
 
-                // if PresentationUniqueKey is empty it's likely a new item.
-                if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
+                if (personEntity is null)
                 {
+                    var path = Person.GetPath(person.Name);
+                    personEntity = new Person()
+                    {
+                        Name = person.Name,
+                        Id = GetItemByNameId<Person>(path),
+                        DateCreated = DateTime.UtcNow,
+                        DateModified = DateTime.UtcNow,
+                        Path = path
+                    };
+
                     personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
                     saveEntity = true;
                 }
@@ -3134,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
             }
 
             var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
-                .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
+                .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
                 .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
 
             if (!string.IsNullOrEmpty(shortcut))

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

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

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

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

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

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

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

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

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

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

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

@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
     /// <summary>
     /// Class MovieResolver.
     /// </summary>
-    public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
+    public partial class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
     {
         private readonly IImageProcessor _imageProcessor;
 
@@ -56,6 +56,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         /// <value>The priority.</value>
         public override ResolverPriority Priority => ResolverPriority.Fourth;
 
+        [GeneratedRegex(@"\bsample\b", RegexOptions.IgnoreCase)]
+        private static partial Regex IsIgnoredRegex();
+
         /// <inheritdoc />
         public MultiItemResolverResult ResolveMultiple(
             Folder parent,
@@ -261,7 +264,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 {
                     leftOver.Add(child);
                 }
-                else if (!IsIgnored(child.Name))
+                else if (!IsIgnoredRegex().IsMatch(child.Name))
                 {
                     files.Add(child);
                 }
@@ -314,9 +317,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return result;
         }
 
-        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)
         {
             for (var i = 0; i < result.Count; i++)

+ 13 - 15
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -1,7 +1,4 @@
-#nullable disable
-
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using Emby.Naming.Common;
@@ -25,7 +22,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         private readonly NamingOptions _namingOptions;
         private readonly IDirectoryService _directoryService;
 
-        private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        private static readonly string[] _ignoreFiles = new[]
         {
             "folder",
             "thumb",
@@ -56,7 +53,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         /// </summary>
         /// <param name="args">The args.</param>
         /// <returns>Trailer.</returns>
-        protected override Photo Resolve(ItemResolveArgs args)
+        protected override Photo? Resolve(ItemResolveArgs args)
         {
             if (!args.IsDirectory)
             {
@@ -68,10 +65,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 {
                     if (IsImageFile(args.Path, _imageProcessor))
                     {
-                        var filename = Path.GetFileNameWithoutExtension(args.Path);
+                        var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
 
                         // Make sure the image doesn't belong to a video file
-                        var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+                        var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+                            ?? throw new InvalidOperationException("Path can't be a root directory."));
 
                         foreach (var file in files)
                         {
@@ -92,32 +90,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
             return null;
         }
 
-        internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+        internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
         {
             return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
         }
 
-        internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+        internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
 
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
         {
             ArgumentNullException.ThrowIfNull(path);
 
-            var filename = Path.GetFileNameWithoutExtension(path);
-
-            if (_ignoreFiles.Contains(filename))
+            var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+            if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
             {
                 return false;
             }
 
-            if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+            var filename = Path.GetFileNameWithoutExtension(path);
+
+            if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
             {
                 return false;
             }
 
-            string extension = Path.GetExtension(path).TrimStart('.');
-            return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+            return true;
         }
     }
 }

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

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

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

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

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

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

+ 6 - 6
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -3,6 +3,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
@@ -16,21 +17,20 @@ using MediaBrowser.Controller.LiveTv;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
     public abstract class BaseTunerHost
     {
-        private readonly IMemoryCache _memoryCache;
+        private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
 
-        protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
+        protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem)
         {
             Config = config;
             Logger = logger;
-            _memoryCache = memoryCache;
             FileSystem = fileSystem;
+            _cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
         }
 
         protected IServerConfigurationManager Config { get; }
@@ -51,7 +51,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var key = tuner.Id;
 
-            if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
+            if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
             {
                 return cache;
             }
@@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
 
             if (!string.IsNullOrEmpty(key) && list.Count > 0)
             {
-                _memoryCache.Set(key, list);
+                _cache[key] = list;
             }
 
             return list;

+ 38 - 41
Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Linq;
 using System.Net;
 using System.Net.Http;
+using System.Net.Http.Json;
 using System.Text.Json;
 using System.Threading;
 using System.Threading.Tasks;
@@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             ISocketFactory socketFactory,
-            IStreamHelper streamHelper,
-            IMemoryCache memoryCache)
-            : base(config, logger, fileSystem, memoryCache)
+            IStreamHelper streamHelper)
+            : base(config, logger, fileSystem)
         {
             _httpClientFactory = httpClientFactory;
             _appHost = appHost;
@@ -77,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
 
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
-                .ConfigureAwait(false) ?? new List<Channels>();
-
+            var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
             if (info.ImportFavoritesOnly)
             {
-                lineup = lineup.Where(i => i.Favorite).ToList();
+                lineup = lineup.Where(i => i.Favorite);
             }
 
             return lineup.Where(i => !i.DRM).ToList();
@@ -130,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                     .ConfigureAwait(false);
                 response.EnsureSuccessStatusCode();
-                await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-                var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
-                    .ConfigureAwait(false);
+                var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
 
                 if (!string.IsNullOrEmpty(cacheKey))
                 {
@@ -176,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
                 .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
                 .ConfigureAwait(false);
-            await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
-            using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
             var tuners = new List<LiveTvTunerInfo>();
-            await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
+            var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+            await using (stream.ConfigureAwait(false))
             {
-                string stripedLine = StripXML(line);
-                if (stripedLine.Contains("Channel", StringComparison.Ordinal))
+                using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
+                await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
                 {
-                    LiveTvTunerStatus status;
-                    var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
-                    var name = stripedLine.Substring(0, index - 1);
-                    var currentChannel = stripedLine.Substring(index + 7);
-                    if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+                    string stripedLine = StripXML(line);
+                    if (stripedLine.Contains("Channel", StringComparison.Ordinal))
                     {
-                        status = LiveTvTunerStatus.LiveTv;
-                    }
-                    else
-                    {
-                        status = LiveTvTunerStatus.Available;
-                    }
+                        LiveTvTunerStatus status;
+                        var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
+                        var name = stripedLine.Substring(0, index - 1);
+                        var currentChannel = stripedLine.Substring(index + 7);
+                        if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
+                        {
+                            status = LiveTvTunerStatus.LiveTv;
+                        }
+                        else
+                        {
+                            status = LiveTvTunerStatus.Available;
+                        }
 
-                    tuners.Add(new LiveTvTunerInfo
-                    {
-                        Name = name,
-                        SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
-                        ProgramName = currentChannel,
-                        Status = status
-                    });
+                        tuners.Add(new LiveTvTunerInfo
+                        {
+                            Name = name,
+                            SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
+                            ProgramName = currentChannel,
+                            Status = status
+                        });
+                    }
                 }
             }
 
@@ -661,18 +658,18 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                 // Need a way to set the Receive timeout on the socket otherwise this might never timeout?
                 try
                 {
-                    await udpClient.SendToAsync(discBytes, 0, discBytes.Length, new IPEndPoint(IPAddress.Parse("255.255.255.255"), 65001), cancellationToken).ConfigureAwait(false);
+                    await udpClient.SendToAsync(discBytes, new IPEndPoint(IPAddress.Broadcast, 65001), cancellationToken).ConfigureAwait(false);
                     var receiveBuffer = new byte[8192];
 
                     while (!cancellationToken.IsCancellationRequested)
                     {
-                        var response = await udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false);
-                        var deviceIp = response.RemoteEndPoint.Address.ToString();
+                        var response = await udpClient.ReceiveMessageFromAsync(receiveBuffer, new IPEndPoint(IPAddress.Any, 0), cancellationToken).ConfigureAwait(false);
+                        var deviceIP = ((IPEndPoint)response.RemoteEndPoint).Address.ToString();
 
-                        // check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
-                        if (response.ReceivedBytes > 13 && response.Buffer[1] == 3)
+                        // Check to make sure we have enough bytes received to be a valid message and make sure the 2nd byte is the discover reply byte
+                        if (response.ReceivedBytes > 13 && receiveBuffer[1] == 3)
                         {
-                            var deviceAddress = "http://" + deviceIp;
+                            var deviceAddress = "http://" + deviceIP;
 
                             var info = await TryGetTunerHostInfo(deviceAddress, cancellationToken).ConfigureAwait(false);
 

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

@@ -44,14 +44,12 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                     StopStreaming(socket).GetAwaiter().GetResult();
                 }
             }
-
-            GC.SuppressFinalize(this);
         }
 
-        public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken)
+        public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
         {
             using var client = new TcpClient();
-            await client.ConnectAsync(remoteIp, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
+            await client.ConnectAsync(remoteIP, HdHomeRunPort, cancellationToken).ConfigureAwait(false);
 
             using var stream = client.GetStream();
             return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false);
@@ -75,9 +73,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
         }
 
-        public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
+        public async Task StartStreaming(IPAddress remoteIP, IPAddress localIP, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken)
         {
-            _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
+            _remoteEndPoint = new IPEndPoint(remoteIP, HdHomeRunPort);
 
             _tcpClient = new TcpClient();
             await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
@@ -125,7 +123,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
                         }
                     }
 
-                    var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort);
+                    var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIP, localPort);
                     var targetMsgLen = WriteSetMessage(buffer, i, "target", targetValue, lockKeyValue);
 
                     await stream.WriteAsync(buffer.AsMemory(0, targetMsgLen), cancellationToken).ConfigureAwait(false);

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

@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 {
-    public class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
+    public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands
     {
         private string? _channel;
         private string? _program;
@@ -13,7 +13,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
         public LegacyHdHomerunChannelCommands(string url)
         {
             // parse url for channel and program
-            var match = Regex.Match(url, @"\/ch([0-9]+)-?([0-9]*)");
+            var match = ChannelAndProgramRegex().Match(url);
             if (match.Success)
             {
                 _channel = match.Groups[1].Value;
@@ -21,6 +21,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             }
         }
 
+        [GeneratedRegex(@"\/ch([0-9]+)-?([0-9]*)")]
+        private static partial Regex ChannelAndProgramRegex();
+
         public IEnumerable<(string CommandName, string CommandValue)> GetCommands()
         {
             if (!string.IsNullOrEmpty(_channel))

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

@@ -5,7 +5,6 @@
 using System;
 using System.Collections.Generic;
 using System.Globalization;
-using System.IO;
 using System.Linq;
 using System.Net.Http;
 using System.Threading;
@@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 using MediaBrowser.Model.LiveTv;
 using MediaBrowser.Model.MediaInfo;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Logging;
 using Microsoft.Net.Http.Headers;
 
@@ -54,9 +52,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             IHttpClientFactory httpClientFactory,
             IServerApplicationHost appHost,
             INetworkManager networkManager,
-            IStreamHelper streamHelper,
-            IMemoryCache memoryCache)
-            : base(config, logger, fileSystem, memoryCache)
+            IStreamHelper streamHelper)
+            : base(config, logger, fileSystem)
         {
             _httpClientFactory = httpClientFactory;
             _appHost = appHost;

+ 7 - 11
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -20,7 +20,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.LiveTv.TunerHosts
 {
-    public class M3uParser
+    public partial class M3uParser
     {
         private const string ExtInfPrefix = "#EXTINF:";
 
@@ -33,6 +33,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             _httpClientFactory = httpClientFactory;
         }
 
+        [GeneratedRegex(@"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase, "en-US")]
+        private static partial Regex KeyValueRegex();
+
         public async Task<List<ChannelInfo>> Parse(TunerHostInfo info, string channelIdPrefix, CancellationToken cancellationToken)
         {
             // Read the file and display it line by line.
@@ -91,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
                 {
                     var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
-                    if (string.IsNullOrWhiteSpace(channel.Id))
-                    {
-                        channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-                    }
-                    else
-                    {
-                        channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
-                    }
+                    channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 
                     channel.Path = trimmedLine;
                     channels.Add(channel);
@@ -311,7 +307,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
         {
             var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
 
-            var matches = Regex.Matches(line, @"([a-z0-9\-_]+)=\""([^""]+)\""", RegexOptions.IgnoreCase);
+            var matches = KeyValueRegex().Matches(line);
 
             remaining = line;
 
@@ -320,7 +316,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
                 var key = match.Groups[1].Value;
                 var value = match.Groups[2].Value;
 
-                dict[match.Groups[1].Value] = match.Groups[2].Value;
+                dict[key] = value;
                 remaining = remaining.Replace(key + "=\"" + value + "\"", string.Empty, StringComparison.OrdinalIgnoreCase);
             }
 

+ 43 - 1
Emby.Server.Implementations/Localization/Core/as.json

@@ -1 +1,43 @@
-{}
+{
+    "Albums": "এলবাম",
+    "Application": "আবেদন",
+    "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
+    "Artists": "শিল্পী",
+    "Channels": "চেনেলস",
+    "Default": "ডিফল্ট",
+    "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+    "Books": "পুস্তক",
+    "Movies": "চলচ্চিত্ৰ",
+    "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+    "Collections": "সংগ্রহ",
+    "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+    "Latest": "শেহতীয়া",
+    "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+    "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+    "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+    "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+    "External": "বাহ্যিক",
+    "Favorites": "পছন্দসই",
+    "Folders": "ফোল্ডাৰ",
+    "Forced": "বলপূর্বক",
+    "Genres": "শ্রেণী",
+    "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+    "HeaderContinueWatching": "দেখা চালিয়ে যান",
+    "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+    "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+    "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+    "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+    "HeaderFavoriteSongs": "প্ৰিয় গীত",
+    "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+    "HeaderNextUp": "পৰৱৰ্তী অংশ",
+    "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+    "HearingImpaired": "শ্ৰবণ অক্ষম",
+    "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+    "Inherit": "উত্তপ্ত কৰা",
+    "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+    "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+    "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+    "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+    "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+    "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}

+ 52 - 0
Emby.Server.Implementations/Localization/Core/chr.json

@@ -0,0 +1,52 @@
+{
+    "ChapterNameValue": "Didanedi {0}",
+    "HeaderAlbumArtists": "Didanidanolisgisgi",
+    "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+    "HeaderLiveTV": "Anigadi didanidisgosgi",
+    "HeaderRecordingGroups": "Didanisquodiisgisgi",
+    "HomeVideos": "Diganadi dinagadisgisgi",
+    "Inherit": "Anigwe",
+    "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+    "MixedContent": "Ganinidi dininoladisgisgi",
+    "Movies": "Anidvnisgisgi",
+    "MusicVideos": "Danodisgisgi didanidisgosgi",
+    "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+    "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+    "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+    "Albums": "Anigawidaniyv",
+    "Application": "Didanvyi",
+    "Artists": "Dinidaniyi",
+    "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+    "Books": "Didanedi",
+    "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+    "Channels": "Diganadasgi",
+    "Collections": "Diganadisgi",
+    "Default": "Dinadi",
+    "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+    "External": "Amohdi",
+    "Favorites": "Nvdayelvdisgi",
+    "Folders": "Didanididisgi",
+    "Forced": "Ganedi",
+    "Genres": "Diganadisgi",
+    "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+    "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+    "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+    "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+    "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+    "HeaderNextUp": "Anidvli uwodoli",
+    "HearingImpaired": "Anitsunidi talunidisgisgi",
+    "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+    "Latest": "Uwodoli",
+    "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+    "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+    "Music": "Danodisgisgi",
+    "NameSeasonUnknown": "Tsunita anidvdisgi",
+    "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+    "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+    "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+    "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+    "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+    "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+    "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+    "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}

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

@@ -22,7 +22,7 @@
     "HeaderFavoriteEpisodes": "Oblíbené epizody",
     "HeaderFavoriteShows": "Oblíbené seriály",
     "HeaderFavoriteSongs": "Oblíbená hudba",
-    "HeaderLiveTV": "Televize",
+    "HeaderLiveTV": "Živý přenos",
     "HeaderNextUp": "Další díly",
     "HeaderRecordingGroups": "Skupiny nahrávek",
     "HomeVideos": "Domácí videa",

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

@@ -15,13 +15,13 @@
     "Favorites": "Favoritter",
     "Folders": "Mapper",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Albums kunstnere",
+    "HeaderAlbumArtists": "Albumkunstnere",
     "HeaderContinueWatching": "Fortsæt afspilning",
-    "HeaderFavoriteAlbums": "Favorit albummer",
-    "HeaderFavoriteArtists": "Favorit kunstnere",
-    "HeaderFavoriteEpisodes": "Favorit afsnit",
-    "HeaderFavoriteShows": "Favorit serier",
-    "HeaderFavoriteSongs": "Favorit sange",
+    "HeaderFavoriteAlbums": "Favoritalbummer",
+    "HeaderFavoriteArtists": "Favoritkunstnere",
+    "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+    "HeaderFavoriteShows": "Yndlingsserier",
+    "HeaderFavoriteSongs": "Yndlingssange",
     "HeaderLiveTV": "Live-TV",
     "HeaderNextUp": "Næste",
     "HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
     "Latest": "Seneste",
     "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
     "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
-    "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+    "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
     "MixedContent": "Blandet indhold",
     "Movies": "Film",
     "Music": "Musik",
@@ -51,7 +51,7 @@
     "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
     "NotificationOptionInstallationFailed": "Installationen mislykkedes",
     "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
-    "NotificationOptionPluginError": "Plugin fejl",
+    "NotificationOptionPluginError": "Plugin-fejl",
     "NotificationOptionPluginInstalled": "Plugin blev installeret",
     "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
     "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
     "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
     "ValueSpecialEpisodeName": "Special - {0}",
     "VersionNumber": "Version {0}",
-    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+    "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
     "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
     "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
     "TaskUpdatePlugins": "Opdater Plugins",
-    "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
-    "TaskCleanLogs": "Ryd Log mappe",
-    "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
-    "TaskRefreshLibrary": "Scan Medie Bibliotek",
-    "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
-    "TaskCleanCache": "Ryd Cache mappe",
-    "TasksChannelsCategory": "Internet Kanaler",
+    "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+    "TaskCleanLogs": "Ryd Log-mappe",
+    "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+    "TaskRefreshLibrary": "Scan Mediebibliotek",
+    "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+    "TaskCleanCache": "Ryd Cache-mappe",
+    "TasksChannelsCategory": "Internetkanaler",
     "TasksApplicationCategory": "Applikation",
     "TasksLibraryCategory": "Bibliotek",
     "TasksMaintenanceCategory": "Vedligeholdelse",
-    "TaskRefreshChapterImages": "Udtræk kapitel billeder",
-    "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
-    "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+    "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+    "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+    "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
     "TaskRefreshChannels": "Opdater Kanaler",
-    "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
-    "TaskCleanTranscode": "Tøm Transcode mappen",
+    "TaskCleanTranscodeDescription": "Fjerner 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.",
@@ -121,8 +121,8 @@
     "Default": "Standard",
     "TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
     "TaskOptimizeDatabase": "Optimér database",
-    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
-    "TaskKeyframeExtractor": "Nøglebillede udtræk",
+    "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+    "TaskKeyframeExtractor": "Udtræk af nøglebillede",
     "External": "Ekstern",
     "HearingImpaired": "Hørehæmmet"
 }

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

@@ -3,9 +3,9 @@
     "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
     "Application": "Aplicación",
     "Artists": "Artistas",
-    "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+    "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
     "Books": "Libros",
-    "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+    "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
     "Channels": "Canales",
     "ChapterNameValue": "Capítulo {0}",
     "Collections": "Colecciones",

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

@@ -74,16 +74,16 @@
     "Shows": "Sarjat",
     "ServerNameNeedsToBeRestarted": "\"{0}\" on käynnistettävä uudelleen",
     "ProviderValue": "Lähde: {0}",
-    "Plugin": "Laajennus",
+    "Plugin": "Lisäosa",
     "NotificationOptionVideoPlaybackStopped": "Videon toisto lopetettu",
     "NotificationOptionVideoPlayback": "Videon toisto aloitettu",
     "NotificationOptionUserLockedOut": "Käyttäjä on lukittu",
     "NotificationOptionTaskFailed": "Ajoitettu tehtävä epäonnistui",
     "NotificationOptionServerRestartRequired": "Tarvitaan palvelimen uudelleenkäynnistys",
-    "NotificationOptionPluginUpdateInstalled": "Laajennus on päivitetty",
-    "NotificationOptionPluginUninstalled": "Laajennus on poistettu",
-    "NotificationOptionPluginInstalled": "Laajennus on asennettu",
-    "NotificationOptionPluginError": "Laajennuksen virhe",
+    "NotificationOptionPluginUpdateInstalled": "Lisäosa päivitettiin",
+    "NotificationOptionPluginUninstalled": "Lisäosa poistettiin",
+    "NotificationOptionPluginInstalled": "Lisäosa asennettiin",
+    "NotificationOptionPluginError": "Lisäosan virhe",
     "NotificationOptionNewLibraryContent": "Sisältöä on lisätty",
     "NotificationOptionInstallationFailed": "Asennus epäonnistui",
     "NotificationOptionCameraImageUploaded": "Kameran kuva on tallennettu",
@@ -98,8 +98,8 @@
     "TaskRefreshChannels": "Päivitä kanavat",
     "TaskCleanTranscodeDescription": "Poistaa päivää vanhemmat transkoodaustiedostot.",
     "TaskCleanTranscode": "Puhdista transkoodauskansio",
-    "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset laajennuksille, jotka on määritetty päivittymään automaattisesti.",
-    "TaskUpdatePlugins": "Päivitä laajennukset",
+    "TaskUpdatePluginsDescription": "Lataa ja asentaa päivitykset lisäosille, jotka on määritetty päivittymään automaattisesti.",
+    "TaskUpdatePlugins": "Päivitä lisäosat",
     "TaskRefreshPeopleDescription": "Päivittää mediakirjaston näyttelijöiden ja ohjaajien metatiedot.",
     "TaskRefreshPeople": "Päivitä henkilöt",
     "TaskCleanLogsDescription": "Poistaa {0} päivää vanhemmat lokitiedostot.",

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

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

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

@@ -105,8 +105,8 @@
     "TaskRefreshPeople": "Actualiser les acteurs",
     "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
     "TaskCleanLogs": "Nettoyer le répertoire des journaux",
-    "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
-    "TaskRefreshLibrary": "Scanner la médiathèque",
+    "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+    "TaskRefreshLibrary": "Analyser la médiathèque",
     "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
     "TaskRefreshChapterImages": "Extraire les images de chapitre",
     "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

+ 21 - 21
Emby.Server.Implementations/Localization/Core/he.json

@@ -5,18 +5,18 @@
     "Artists": "אומנים",
     "AuthenticationSucceededWithUserName": "{0} אומת בהצלחה",
     "Books": "ספרים",
-    "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מ {0}",
+    "CameraImageUploadedFrom": "תמונת מצלמה חדשה הועלתה מתוך {0}",
     "Channels": "ערוצים",
     "ChapterNameValue": "פרק {0}",
     "Collections": "אוספים",
     "DeviceOfflineWithName": "{0} התנתק",
     "DeviceOnlineWithName": "{0} מחובר",
-    "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי מ{0}",
+    "FailedLoginAttemptWithUserName": "ניסיון כניסה שגוי דרך {0}",
     "Favorites": "מועדפים",
     "Folders": "תיקיות",
-    "Genres": "ז'אנרים",
+    "Genres": "ז׳אנרים",
     "HeaderAlbumArtists": "אמני האלבום",
-    "HeaderContinueWatching": "המשך לצפות",
+    "HeaderContinueWatching": "להמשיך לצפות",
     "HeaderFavoriteAlbums": "אלבומים מועדפים",
     "HeaderFavoriteArtists": "אמנים מועדפים",
     "HeaderFavoriteEpisodes": "פרקים מועדפים",
@@ -27,14 +27,14 @@
     "HeaderRecordingGroups": "קבוצות הקלטה",
     "HomeVideos": "סרטונים בייתים",
     "Inherit": "הורש",
-    "ItemAddedWithName": "{0} הוסף לספרייה",
+    "ItemAddedWithName": "{0} נוסף לספרייה",
     "ItemRemovedWithName": "{0} נמחק מהספרייה",
     "LabelIpAddressValue": "Ip כתובת: {0}",
     "LabelRunningTimeValue": "משך צפייה: {0}",
     "Latest": "אחרון",
     "MessageApplicationUpdated": "שרת הJellyfin עודכן",
-    "MessageApplicationUpdatedTo": "שרת הJellyfin עודכן לגרסא {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "הגדרת השרת {0} שונתה",
+    "MessageApplicationUpdatedTo": "שרת ה־Jellyfin עודכן לגרסה {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "סעיף הגדרת השרת {0} עודכן",
     "MessageServerConfigurationUpdated": "תצורת השרת עודכנה",
     "MixedContent": "תוכן מעורב",
     "Movies": "סרטים",
@@ -50,7 +50,7 @@
     "NotificationOptionAudioPlaybackStopped": "ניגון שמע הופסק",
     "NotificationOptionCameraImageUploaded": "תמונת מצלמה הועלתה",
     "NotificationOptionInstallationFailed": "התקנה נכשלה",
-    "NotificationOptionNewLibraryContent": "תוכן חדש הוסף",
+    "NotificationOptionNewLibraryContent": "תוכן חדש נוסף",
     "NotificationOptionPluginError": "כשלון בתוסף",
     "NotificationOptionPluginInstalled": "התוסף הותקן",
     "NotificationOptionPluginUninstalled": "התוסף הוסר",
@@ -61,41 +61,41 @@
     "NotificationOptionVideoPlayback": "ניגון וידאו החל",
     "NotificationOptionVideoPlaybackStopped": "ניגון וידאו הופסק",
     "Photos": "תמונות",
-    "Playlists": "רשימות הפעלה",
-    "Plugin": "Plugin",
+    "Playlists": "רשימות נגינה",
+    "Plugin": "תוסף",
     "PluginInstalledWithName": "{0} הותקן",
     "PluginUninstalledWithName": "{0} הוסר",
     "PluginUpdatedWithName": "{0} עודכן",
-    "ProviderValue": "Provider: {0}",
+    "ProviderValue": "ספק: {0}",
     "ScheduledTaskFailedWithName": "{0} נכשל",
     "ScheduledTaskStartedWithName": "{0} החל",
     "ServerNameNeedsToBeRestarted": "{0} דורש הפעלה מחדש",
     "Shows": "סדרות",
     "Songs": "שירים",
-    "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. אנא נסה שנית בעוד זמן קצר.",
+    "StartupEmbyServerIsLoading": "שרת Jellyfin בהליכי טעינה. נא לנסות שנית בהקדם.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "הורדת כתוביות נכשלה מ-{0} עבור {1}",
-    "Sync": "סנכרן",
-    "System": "System",
+    "SubtitleDownloadFailureFromForItem": "הורדת כתוביות מ־{0} עבור {1} נכשלה",
+    "Sync": "סנכרון",
+    "System": "מערכת",
     "TvShows": "סדרות טלוויזיה",
-    "User": "User",
+    "User": "משתמש",
     "UserCreatedWithName": "המשתמש {0} נוצר",
     "UserDeletedWithName": "המשתמש {0} הוסר",
     "UserDownloadingItemWithValues": "{0} מוריד את {1}",
     "UserLockedOutWithName": "המשתמש {0} ננעל",
-    "UserOfflineFromDevice": "{0} התנתק מ-{1}",
-    "UserOnlineFromDevice": "{0} מחובר מ-{1}",
+    "UserOfflineFromDevice": "{0} התנתק מ־{1}",
+    "UserOnlineFromDevice": "{0} מחובר מ־{1}",
     "UserPasswordChangedWithName": "הסיסמה שונתה עבור המשתמש {0}",
     "UserPolicyUpdatedWithName": "מדיניות המשתמש {0} עודכנה",
     "UserStartedPlayingItemWithValues": "{0} מנגן את {1} על {2}",
     "UserStoppedPlayingItemWithValues": "{0} סיים לנגן את {1} על {2}",
     "ValueHasBeenAddedToLibrary": "{0} התווסף לספריית המדיה שלך",
     "ValueSpecialEpisodeName": "מיוחד- {0}",
-    "VersionNumber": "Version {0}",
+    "VersionNumber": "גרסה {0}",
     "TaskRefreshLibrary": "סרוק ספריית מדיה",
     "TaskRefreshChapterImages": "חלץ תמונות פרקים",
     "TaskCleanCacheDescription": "מחק קבצי מטמון שלא בשימוש המערכת.",
-    "TaskCleanCache": "נקה תיקיית מטמון",
+    "TaskCleanCache": "ניקוי תיקיית מטמון",
     "TasksApplicationCategory": "יישום",
     "TasksLibraryCategory": "ספרייה",
     "TasksMaintenanceCategory": "תחזוקה",
@@ -103,7 +103,7 @@
     "TaskRefreshPeopleDescription": "מעדכן מטא נתונים עבור שחקנים ובמאים בספריית המדיה שלך.",
     "TaskRefreshPeople": "רענן אנשים",
     "TaskCleanLogsDescription": "מוחק קבצי יומן בני יותר מ- {0} ימים.",
-    "TaskCleanLogs": "נקה תיקיית יומן",
+    "TaskCleanLogs": "ניקוי תיקיית יומן",
     "TaskRefreshLibraryDescription": "סורק את ספריית המדיה שלך אחר קבצים חדשים ומרענן מטא נתונים.",
     "TaskRefreshChapterImagesDescription": "יוצר תמונות ממוזערות לסרטונים שיש להם פרקים.",
     "TasksChannelsCategory": "ערוצי אינטרנט",

+ 41 - 41
Emby.Server.Implementations/Localization/Core/hu.json

@@ -1,11 +1,11 @@
 {
     "Albums": "Albumok",
-    "AppDeviceValues": "Program: {0}, Eszköz: {1}",
+    "AppDeviceValues": "Program: {0}, eszköz: {1}",
     "Application": "Alkalmazás",
     "Artists": "Előadók",
-    "AuthenticationSucceededWithUserName": "{0} sikeresen azonosítva",
+    "AuthenticationSucceededWithUserName": "{0} sikeresen hitelesítve",
     "Books": "Könyvek",
-    "CameraImageUploadedFrom": "Új kamerakép került feltöltésre innen: {0}",
+    "CameraImageUploadedFrom": "Új kamerakép feltöltve innen: {0}",
     "Channels": "Csatornák",
     "ChapterNameValue": "{0}. jelenet",
     "Collections": "Gyűjtemények",
@@ -15,13 +15,13 @@
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
-    "HeaderAlbumArtists": "Album előadó(k)",
+    "HeaderAlbumArtists": "Albumelőadók",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",
     "HeaderFavoriteEpisodes": "Kedvenc epizódok",
     "HeaderFavoriteShows": "Kedvenc sorozatok",
-    "HeaderFavoriteSongs": "Kedvenc dalok",
+    "HeaderFavoriteSongs": "Kedvenc számok",
     "HeaderLiveTV": "Élő TV",
     "HeaderNextUp": "Következik",
     "HeaderRecordingGroups": "Felvételi csoportok",
@@ -29,37 +29,37 @@
     "Inherit": "Örökölt",
     "ItemAddedWithName": "{0} hozzáadva a könyvtárhoz",
     "ItemRemovedWithName": "{0} eltávolítva a könyvtárból",
-    "LabelIpAddressValue": "IP cím: {0}",
-    "LabelRunningTimeValue": "Futási idő: {0}",
+    "LabelIpAddressValue": "IP-cím: {0}",
+    "LabelRunningTimeValue": "Lejátszási idő: {0}",
     "Latest": "Legújabb",
-    "MessageApplicationUpdated": "Jellyfin Szerver frissítve",
-    "MessageApplicationUpdatedTo": "Jellyfin Szerver frissítve lett a következőre: {0}",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Szerver konfigurációs rész frissítve: {0}",
-    "MessageServerConfigurationUpdated": "Szerver konfiguráció frissítve",
+    "MessageApplicationUpdated": "A Jellyfin kiszolgáló frissítve",
+    "MessageApplicationUpdatedTo": "A Jellyfin kiszolgáló frissítve lett a következőre: {0}",
+    "MessageNamedServerConfigurationUpdatedWithValue": "A kiszolgálókonfigurációs rész frissítve: {0}",
+    "MessageServerConfigurationUpdated": "Kiszolgálókonfiguráció frissítve",
     "MixedContent": "Vegyes tartalom",
     "Movies": "Filmek",
-    "Music": "Zene",
+    "Music": "Zenék",
     "MusicVideos": "Zenei videóklippek",
     "NameInstallFailed": "{0} sikertelen telepítés",
     "NameSeasonNumber": "{0}. évad",
     "NameSeasonUnknown": "Ismeretlen évad",
-    "NewVersionIsAvailable": "Letölthető a Jellyfin Szerver új verziója.",
+    "NewVersionIsAvailable": "Letölthető a Jellyfin kiszolgáló új verziója.",
     "NotificationOptionApplicationUpdateAvailable": "Frissítés érhető el az alkalmazáshoz",
     "NotificationOptionApplicationUpdateInstalled": "Alkalmazásfrissítés telepítve",
-    "NotificationOptionAudioPlayback": "Audió lejátszás elkezdve",
-    "NotificationOptionAudioPlaybackStopped": "Audió lejátszás leállítva",
-    "NotificationOptionCameraImageUploaded": "Kamera kép feltöltve",
-    "NotificationOptionInstallationFailed": "Telepítés sikertelen",
+    "NotificationOptionAudioPlayback": "Hanglejátszás elkezdve",
+    "NotificationOptionAudioPlaybackStopped": "Hanglejátszás leállítva",
+    "NotificationOptionCameraImageUploaded": "Kamerakép feltöltve",
+    "NotificationOptionInstallationFailed": "Telepítési hiba",
     "NotificationOptionNewLibraryContent": "Új tartalom hozzáadva",
-    "NotificationOptionPluginError": "Bővítmény hiba",
+    "NotificationOptionPluginError": "Bővítményhiba",
     "NotificationOptionPluginInstalled": "Bővítmény telepítve",
     "NotificationOptionPluginUninstalled": "Bővítmény eltávolítva",
-    "NotificationOptionPluginUpdateInstalled": "Bővítmény frissítés telepítve",
-    "NotificationOptionServerRestartRequired": "Szerver újraindítás szükséges",
+    "NotificationOptionPluginUpdateInstalled": "Bővítményfrissítés telepítve",
+    "NotificationOptionServerRestartRequired": "A kiszolgáló újraindítása szükséges",
     "NotificationOptionTaskFailed": "Ütemezett feladat hiba",
     "NotificationOptionUserLockedOut": "Felhasználó tiltva",
-    "NotificationOptionVideoPlayback": "Videó lejátszás elkezdve",
-    "NotificationOptionVideoPlaybackStopped": "Videó lejátszás leállítva",
+    "NotificationOptionVideoPlayback": "Videólejátszás elkezdve",
+    "NotificationOptionVideoPlaybackStopped": "Videólejátszás leállítva",
     "Photos": "Fényképek",
     "Playlists": "Lejátszási listák",
     "Plugin": "Bővítmény",
@@ -69,47 +69,47 @@
     "ProviderValue": "Szolgáltató: {0}",
     "ScheduledTaskFailedWithName": "{0} sikertelen",
     "ScheduledTaskStartedWithName": "{0} elkezdve",
-    "ServerNameNeedsToBeRestarted": "{0}-t újra kell indítani",
+    "ServerNameNeedsToBeRestarted": "A(z) {0} újraindítása szükséges",
     "Shows": "Sorozatok",
-    "Songs": "Dalok",
-    "StartupEmbyServerIsLoading": "A Jellyfin Szerver betöltődik. Kérlek, próbáld újra hamarosan.",
+    "Songs": "Számok",
+    "StartupEmbyServerIsLoading": "A Jellyfin kiszolgáló betöltődik. Próbálja újra hamarosan.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0} ehhez: {1}",
-    "Sync": "Szinkronizál",
+    "SubtitleDownloadFailureFromForItem": "Nem sikerült a felirat letöltése innen: {0}, ehhez: {1}",
+    "Sync": "Szinkronizálás",
     "System": "Rendszer",
     "TvShows": "TV műsorok",
     "User": "Felhasználó",
     "UserCreatedWithName": "{0} felhasználó létrehozva",
     "UserDeletedWithName": "{0} felhasználó törölve",
-    "UserDownloadingItemWithValues": "{0} letölti {1}",
+    "UserDownloadingItemWithValues": "{0} letölti: {1}",
     "UserLockedOutWithName": "{0} felhasználó zárolva van",
     "UserOfflineFromDevice": "{0} kijelentkezett innen: {1}",
     "UserOnlineFromDevice": "{0} online innen: {1}",
-    "UserPasswordChangedWithName": "Jelszó megváltozott a következő felhasználó számára: {0}",
-    "UserPolicyUpdatedWithName": "A felhasználói házirend frissítve lett neki: {0}",
-    "UserStartedPlayingItemWithValues": "{0} elkezdte játszani a következőt: {1} itt: {2}",
-    "UserStoppedPlayingItemWithValues": "{0} befejezte {1} lejátászását itt: {2}",
+    "UserPasswordChangedWithName": "{0} jelszava megváltozott",
+    "UserPolicyUpdatedWithName": "{0} felhasználói házirendje frissült",
+    "UserStartedPlayingItemWithValues": "{0} elkezdte lejátszani a következőt: {1}, itt: {2}",
+    "UserStoppedPlayingItemWithValues": "{0} befejezte a következő lejátszását: {1}, itt: {2}",
     "ValueHasBeenAddedToLibrary": "{0} hozzáadva a médiatárhoz",
-    "ValueSpecialEpisodeName": "Special - {0}",
+    "ValueSpecialEpisodeName": "Különkiadás – {0}",
     "VersionNumber": "Verzió: {0}",
     "TaskCleanTranscode": "Átkódolási könyvtár ürítése",
     "TaskUpdatePluginsDescription": "Letölti és telepíti a frissítéseket azokhoz a bővítményekhez, amelyeknél az automatikus frissítés engedélyezve van.",
     "TaskUpdatePlugins": "Bővítmények frissítése",
-    "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a könyvtáradban.",
+    "TaskRefreshPeopleDescription": "Frissíti a szereplők és a stábok metaadatait a médiatárban.",
     "TaskRefreshPeople": "Személyek frissítése",
     "TaskCleanLogsDescription": "Törli azokat a naplófájlokat, amelyek {0} napnál régebbiek.",
     "TaskCleanLogs": "Naplózási könyvtár ürítése",
-    "TaskRefreshLibraryDescription": "Átvizsgálja a könyvtáraidat új fájlokért és frissíti a metaadatokat.",
-    "TaskRefreshLibrary": "Média könyvtár beolvasása",
-    "TaskRefreshChapterImagesDescription": "Miniatűröket generál olyan videókhoz, amely tartalmaz fejezeteket.",
-    "TaskRefreshChapterImages": "Fejezetek képeinek generálása",
+    "TaskRefreshLibraryDescription": "Átvizsgálja a médiatárat új fájlokat keresve, és frissíti a metaadatokat.",
+    "TaskRefreshLibrary": "Médiatár átvizsgálása",
+    "TaskRefreshChapterImagesDescription": "Miniatűröket hoz létre az olyan videókhoz, amely tartalmaz fejezeteket.",
+    "TaskRefreshChapterImages": "Fejezetképek kinyerése",
     "TaskCleanCacheDescription": "Törli azokat a gyorsítótárazott fájlokat, amikre a rendszernek már nincs szüksége.",
     "TaskCleanCache": "Gyorsítótár könyvtárának ürítése",
     "TasksChannelsCategory": "Internetes csatornák",
     "TasksApplicationCategory": "Alkalmazás",
     "TasksLibraryCategory": "Könyvtár",
     "TasksMaintenanceCategory": "Karbantartás",
-    "TaskDownloadMissingSubtitlesDescription": "A metaadat konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
+    "TaskDownloadMissingSubtitlesDescription": "A metaadat-konfiguráció alapján ellenőrzi és letölti a hiányzó feliratokat az internetről.",
     "TaskDownloadMissingSubtitles": "Hiányzó feliratok letöltése",
     "TaskRefreshChannelsDescription": "Frissíti az internetes csatornák adatait.",
     "TaskRefreshChannels": "Csatornák frissítése",
@@ -121,8 +121,8 @@
     "Default": "Alapértelmezett",
     "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.",
     "TaskOptimizeDatabase": "Adatbázis optimalizálása",
-    "TaskKeyframeExtractor": "Kulcskockák kibontása",
-    "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
+    "TaskKeyframeExtractor": "Kulcsképkockák kibontása",
+    "TaskKeyframeExtractorDescription": "Kibontja a kulcsképkockákat a videófájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat.",
     "External": "Külső",
     "HearingImpaired": "Hallássérült"
 }

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

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

+ 121 - 1
Emby.Server.Implementations/Localization/Core/kn.json

@@ -3,5 +3,125 @@
     "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
     "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
     "TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
-    "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+    "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+    "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+    "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+    "TasksLibraryCategory": "ಸಮೊಹ",
+    "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+    "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+    "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+    "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+    "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+    "Albums": "ಸಂಪುಟ",
+    "Application": "ಅಪ್ಲಿಕೇಶನ್",
+    "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+    "Artists": "ಕಲಾವಿದರು",
+    "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+    "Books": "ಪುಸ್ತಕಗಳು",
+    "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+    "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+    "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+    "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+    "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+    "External": "ಹೊರಗಿನ",
+    "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+    "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+    "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
+    "Forced": "ಬಲವಂತವಾಗಿ",
+    "Genres": "ಪ್ರಕಾರಗಳು",
+    "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+    "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+    "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+    "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+    "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+    "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+    "HeaderNextUp": "ಮುಂದೆ",
+    "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+    "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+    "Channels": "ಮೂಲಗಳು",
+    "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+    "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+    "HearingImpaired": "ಮೂಗ",
+    "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+    "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+    "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
+    "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+    "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+    "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+    "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+    "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+    "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+    "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+    "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+    "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+    "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+    "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+    "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
+    "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+    "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+    "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+    "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+    "VersionNumber": "ಆವೃತ್ತಿ {0}",
+    "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+    "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+    "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+    "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+    "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+    "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+    "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+    "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
+    "Shows": "ಧಾರವಾಹಿಗಳು",
+    "Songs": "ಹಾಡುಗಳು",
+    "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+    "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+    "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+    "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+    "Sync": "ಹೊಂದಿಕೆ",
+    "System": "ವ್ಯವಸ್ಥೆ",
+    "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+    "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+    "User": "ಬಳಕೆದಾರ",
+    "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+    "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+    "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+    "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+    "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+    "Latest": "ಹೊಸದಾದ",
+    "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+    "Movies": "ಚಲನಚಿತ್ರಗಳು",
+    "Music": "ಸಂಗೀತ",
+    "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+    "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+    "NameSeasonNumber": "ಸೀಸನ್ {0}",
+    "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+    "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+    "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+    "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+    "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+    "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+    "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+    "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+    "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+    "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+    "Photos": "ಚಿತ್ರಗಳು",
+    "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+    "Plugin": "ಪ್ಲಗಿನ್",
+    "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+    "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+    "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+    "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+    "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+    "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+    "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
+    "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+    "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+    "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
 }

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

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

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

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

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

@@ -1,5 +1,5 @@
 {
-    "Albums": "Album-album",
+    "Albums": "Album",
     "AppDeviceValues": "Apl: {0}, Peranti: {1}",
     "Application": "Aplikasi",
     "Artists": "Artis-artis",

+ 2 - 2
Emby.Server.Implementations/Localization/Core/nl.json

@@ -1,9 +1,9 @@
 {
     "Albums": "Albums",
     "AppDeviceValues": "App: {0}, Apparaat: {1}",
-    "Application": "Toepassing",
+    "Application": "Applicatie",
     "Artists": "Artiesten",
-    "AuthenticationSucceededWithUserName": "{0} is succesvol geauthenticeerd",
+    "AuthenticationSucceededWithUserName": "{0} succesvol geauthenticeerd",
     "Books": "Boeken",
     "CameraImageUploadedFrom": "Nieuwe camera-afbeelding toegevoegd vanaf {0}",
     "Channels": "Kanalen",

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

@@ -24,5 +24,13 @@
     "TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
     "HeaderAlbumArtists": "Buccaneers o' the musical arts",
     "HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
-    "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
+    "HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
+    "Channels": "Channels",
+    "Forced": "Pressed",
+    "External": "Outboard",
+    "HeaderFavoriteEpisodes": "Treasured Tales",
+    "HeaderFavoriteShows": "Treasured Tales",
+    "ChapterNameValue": "Piece {0}",
+    "HeaderFavoriteSongs": "Treasured Chimes",
+    "HeaderNextUp": "Incoming"
 }

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

@@ -31,13 +31,13 @@
     "ItemRemovedWithName": "{0} - изъято из медиатеки",
     "LabelIpAddressValue": "IP-адрес: {0}",
     "LabelRunningTimeValue": "Длительность: {0}",
-    "Latest": "Новое",
+    "Latest": "Последние добавленные",
     "MessageApplicationUpdated": "Jellyfin Server был обновлён",
     "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
     "MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
     "MixedContent": "Смешанное содержание",
-    "Movies": "Кино",
+    "Movies": "Фильмы",
     "Music": "Музыка",
     "MusicVideos": "Муз. видео",
     "NameInstallFailed": "Установка {0} неудачна",
@@ -77,7 +77,7 @@
     "SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
     "Sync": "Синхронизация",
     "System": "Система",
-    "TvShows": "ТВ",
+    "TvShows": "Телесериалы",
     "User": "Пользователь",
     "UserCreatedWithName": "Пользователь {0} был создан",
     "UserDeletedWithName": "Пользователь {0} был удалён",

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

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

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

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

+ 1 - 1
Emby.Server.Implementations/Localization/Core/sl-SI.json

@@ -11,7 +11,7 @@
     "Collections": "Zbirke",
     "DeviceOfflineWithName": "{0} je prekinil povezavo",
     "DeviceOnlineWithName": "{0} je povezan",
-    "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave iz {0}",
+    "FailedLoginAttemptWithUserName": "Neuspešen poskus prijave z {0}",
     "Favorites": "Priljubljeno",
     "Folders": "Mape",
     "Genres": "Zvrsti",

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

@@ -122,5 +122,6 @@
     "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்",
     "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
     "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
-    "External": "வெளி"
+    "External": "வெளி",
+    "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
 }

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

@@ -121,5 +121,7 @@
     "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล",
     "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น",
     "External": "ภายนอก",
-    "HearingImpaired": "บกพร่องทางการได้ยิน"
+    "HearingImpaired": "บกพร่องทางการได้ยิน",
+    "TaskKeyframeExtractor": "ตัวแยกคีย์เฟรม",
+    "TaskKeyframeExtractorDescription": "แยกคีย์เฟรมจากไฟล์วีดีโอเพื่อสร้างรายการ HLS ให้ถูกต้อง. กระบวนการนี้อาจใช้ระยะเวลานาน"
 }

+ 24 - 24
Emby.Server.Implementations/Localization/Core/tr.json

@@ -3,19 +3,19 @@
     "AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
     "Application": "Uygulama",
     "Artists": "Sanatçılar",
-    "AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
+    "AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
     "Books": "Kitaplar",
     "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
     "Channels": "Kanallar",
-    "ChapterNameValue": "Bölüm {0}",
+    "ChapterNameValue": "{0}. Bölüm",
     "Collections": "Koleksiyonlar",
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOnlineWithName": "{0} bağlı",
-    "FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu",
+    "FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
     "Favorites": "Favoriler",
     "Folders": "Klasörler",
     "Genres": "Türler",
-    "HeaderAlbumArtists": "Albüm Sanatçıları",
+    "HeaderAlbumArtists": "Albüm sanatçıları",
     "HeaderContinueWatching": "İzlemeye Devam Et",
     "HeaderFavoriteAlbums": "Favori Albümler",
     "HeaderFavoriteArtists": "Favori Sanatçılar",
@@ -25,7 +25,7 @@
     "HeaderLiveTV": "Canlı TV",
     "HeaderNextUp": "Gelecek Hafta",
     "HeaderRecordingGroups": "Kayıt Grupları",
-    "HomeVideos": "Ana sayfa videoları",
+    "HomeVideos": "Ana Sayfa Videoları",
     "Inherit": "Devral",
     "ItemAddedWithName": "{0} kütüphaneye eklendi",
     "ItemRemovedWithName": "{0} kütüphaneden silindi",
@@ -34,14 +34,14 @@
     "Latest": "En son",
     "MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
     "MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
-    "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi",
-    "MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi",
+    "MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
+    "MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
     "MixedContent": "Karışık içerik",
     "Movies": "Filmler",
     "Music": "Müzik",
-    "MusicVideos": "Müzik videoları",
+    "MusicVideos": "Müzik Videoları",
     "NameInstallFailed": "{0} kurulumu başarısız",
-    "NameSeasonNumber": "Sezon {0}",
+    "NameSeasonNumber": "{0}. Sezon",
     "NameSeasonUnknown": "Bilinmeyen Sezon",
     "NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
     "NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
@@ -55,9 +55,9 @@
     "NotificationOptionPluginInstalled": "Eklenti yüklendi",
     "NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
     "NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
-    "NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli",
+    "NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılma gerekiyor",
     "NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
-    "NotificationOptionUserLockedOut": "Kullanıcı kitlendi",
+    "NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
     "NotificationOptionVideoPlayback": "Video oynatma başladı",
     "NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
     "Photos": "Fotoğraflar",
@@ -74,36 +74,36 @@
     "Songs": "Şarkılar",
     "StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
     "SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
-    "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
+    "SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
     "Sync": "Eşzamanlama",
     "System": "Sistem",
     "TvShows": "Diziler",
     "User": "Kullanıcı",
     "UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
-    "UserDeletedWithName": "Kullanıcı {0} silindi",
-    "UserDownloadingItemWithValues": "{0} indiriliyor {1}",
-    "UserLockedOutWithName": "Kullanıcı {0} kitlendi",
-    "UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi",
-    "UserOnlineFromDevice": "{0}, {1} çevrimiçi",
-    "UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi",
-    "UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için güncellendi",
+    "UserDeletedWithName": "{0} kullanıcısı silindi",
+    "UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
+    "UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
+    "UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
+    "UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
+    "UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
+    "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
     "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
     "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi",
     "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi",
     "ValueSpecialEpisodeName": "Özel - {0}",
     "VersionNumber": "Sürüm {0}",
-    "TaskCleanCache": "Geçici dosya klasörünü temizle",
-    "TasksChannelsCategory": "İnternet kanalları",
+    "TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
+    "TasksChannelsCategory": "İnternet Kanalları",
     "TasksApplicationCategory": "Uygulama",
     "TasksLibraryCategory": "Kütüphane",
     "TasksMaintenanceCategory": "Bakım",
     "TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
-    "TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
+    "TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
     "TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
     "TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
     "TaskRefreshChannels": "Kanalları Yenile",
-    "TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
-    "TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
+    "TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
+    "TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
     "TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
     "TaskUpdatePlugins": "Eklentileri Güncelle",
     "TaskRefreshPeople": "Kullanıcıları Yenile",

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

@@ -25,5 +25,14 @@
     "Channels": "Amashaneli",
     "Books": "Izincwadi",
     "Artists": "Abadlali",
-    "Albums": "Ama-albhamu"
+    "Albums": "Ama-albhamu",
+    "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+    "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+    "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+    "HeaderFavoriteShows": "Izisho Ezithandekayo",
+    "External": "Kwezifungo",
+    "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+    "HeaderContinueWatching": "Buyela Ukubona",
+    "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+    "HeaderAlbumArtists": "Abasethi wenkulumo"
 }

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

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

+ 5 - 1
Emby.Server.Implementations/Localization/Ratings/au.csv

@@ -4,10 +4,14 @@ G,0
 M,15
 MA,15
 MA15+,15
+MA 15+,15
 PG,16
 16+,16
 R,18
 R18+,18
-X18+,18
+R 18+,18
 18+,18
+X18+,1000
+X 18+,1000
 X,1000
+RC,1001

+ 5 - 0
Emby.Server.Implementations/Localization/Ratings/de.csv

@@ -1,12 +1,17 @@
 Educational,0
 Infoprogramm,0
 FSK-0,0
+FSK 0,0
 0,0
 FSK-6,6
+FSK 6,6
 6,6
 FSK-12,12
+FSK 12,12
 12,12
 FSK-16,16
+FSK 16,16
 16,16
 FSK-18,18
+FSK 18,18
 18,18

+ 1 - 0
Emby.Server.Implementations/Localization/Ratings/es.csv

@@ -3,6 +3,7 @@ A/fig,0
 A/i,0
 A/fig/i,0
 APTA,0
+ERI,0
 TP,0
 0+,0
 6+,6

+ 1 - 0
Emby.Server.Implementations/Localization/Ratings/fr.csv

@@ -1,5 +1,6 @@
 Public Averti,0
 Tous Publics,0
+TP,0
 U,0
 0+,0
 6+,6

+ 6 - 0
Emby.Server.Implementations/Localization/Ratings/sk.csv

@@ -0,0 +1,6 @@
+NR,0
+U,0
+7,7
+12,12
+15,15
+18,18

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