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

Merge branch 'master' into media-type

Cody Robibero 1 жил өмнө
parent
commit
892973a9e3
100 өөрчлөгдсөн 915 нэмэгдсэн , 850 устгасан
  1. 12 0
      .config/dotnet-tools.json
  2. 4 4
      .github/workflows/codeql-analysis.yml
  3. 7 7
      .github/workflows/commands.yml
  4. 4 4
      .github/workflows/openapi.yml
  5. 2 2
      .github/workflows/repo-bump-version.yaml
  6. 5 3
      .github/workflows/repo-stale.yaml
  7. 3 0
      CONTRIBUTORS.md
  8. 20 22
      Directory.Packages.props
  9. 20 32
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  10. 5 1
      Emby.Dlna/Emby.Dlna.csproj
  11. 69 0
      Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
  12. 17 117
      Emby.Dlna/Main/DlnaEntryPoint.cs
  13. 6 11
      Emby.Dlna/PlayTo/Device.cs
  14. 28 27
      Emby.Dlna/PlayTo/DlnaHttpClient.cs
  15. 6 7
      Emby.Dlna/PlayTo/PlayToController.cs
  16. 2 2
      Emby.Dlna/PlayTo/PlayToManager.cs
  17. 5 5
      Emby.Naming/Common/NamingOptions.cs
  18. 5 1
      Emby.Naming/Emby.Naming.csproj
  19. 6 2
      Emby.Photos/Emby.Photos.csproj
  20. 2 4
      Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs
  21. 3 11
      Emby.Server.Implementations/ApplicationHost.cs
  22. 6 3
      Emby.Server.Implementations/Channels/ChannelManager.cs
  23. 43 169
      Emby.Server.Implementations/Data/SqliteItemRepository.cs
  24. 10 1
      Emby.Server.Implementations/Dto/DtoService.cs
  25. 6 3
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  26. 22 13
      Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs
  27. 0 1
      Emby.Server.Implementations/HttpServer/WebSocketConnection.cs
  28. 0 1
      Emby.Server.Implementations/IO/FileRefresher.cs
  29. 1 1
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  30. 1 1
      Emby.Server.Implementations/Images/BaseFolderImageProvider.cs
  31. 28 32
      Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
  32. 3 3
      Emby.Server.Implementations/Images/DynamicImageProvider.cs
  33. 42 37
      Emby.Server.Implementations/Library/LibraryManager.cs
  34. 13 5
      Emby.Server.Implementations/Library/LiveStreamHelper.cs
  35. 10 5
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  36. 7 7
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  37. 2 1
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  38. 2 1
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  39. 2 1
      Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
  40. 30 32
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  41. 3 2
      Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
  42. 3 3
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  43. 3 3
      Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs
  44. 4 2
      Emby.Server.Implementations/Library/Resolvers/SpecialFolderResolver.cs
  45. 4 3
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  46. 1 1
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  47. 4 3
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  48. 17 18
      Emby.Server.Implementations/Library/UserViewManager.cs
  49. 9 5
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  50. 7 18
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  51. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  52. 0 1
      Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs
  53. 30 32
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs
  54. 0 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  55. 0 2
      Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs
  56. 3 1
      Emby.Server.Implementations/Localization/Core/be.json
  57. 2 1
      Emby.Server.Implementations/Localization/Core/bg-BG.json
  58. 3 1
      Emby.Server.Implementations/Localization/Core/cs.json
  59. 3 1
      Emby.Server.Implementations/Localization/Core/de.json
  60. 3 1
      Emby.Server.Implementations/Localization/Core/el.json
  61. 3 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  62. 2 0
      Emby.Server.Implementations/Localization/Core/en-US.json
  63. 3 1
      Emby.Server.Implementations/Localization/Core/es.json
  64. 3 1
      Emby.Server.Implementations/Localization/Core/fi.json
  65. 2 1
      Emby.Server.Implementations/Localization/Core/fil.json
  66. 18 0
      Emby.Server.Implementations/Localization/Core/fo.json
  67. 8 6
      Emby.Server.Implementations/Localization/Core/fr.json
  68. 3 1
      Emby.Server.Implementations/Localization/Core/he.json
  69. 3 1
      Emby.Server.Implementations/Localization/Core/hr.json
  70. 3 1
      Emby.Server.Implementations/Localization/Core/hu.json
  71. 35 28
      Emby.Server.Implementations/Localization/Core/is.json
  72. 3 1
      Emby.Server.Implementations/Localization/Core/it.json
  73. 22 20
      Emby.Server.Implementations/Localization/Core/ja.json
  74. 4 1
      Emby.Server.Implementations/Localization/Core/kk.json
  75. 41 41
      Emby.Server.Implementations/Localization/Core/lv.json
  76. 3 1
      Emby.Server.Implementations/Localization/Core/nl.json
  77. 3 1
      Emby.Server.Implementations/Localization/Core/pl.json
  78. 3 1
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  79. 4 2
      Emby.Server.Implementations/Localization/Core/pt.json
  80. 3 1
      Emby.Server.Implementations/Localization/Core/ru.json
  81. 1 0
      Emby.Server.Implementations/Localization/Core/si.json
  82. 3 1
      Emby.Server.Implementations/Localization/Core/sk.json
  83. 3 1
      Emby.Server.Implementations/Localization/Core/sv.json
  84. 4 2
      Emby.Server.Implementations/Localization/Core/ta.json
  85. 3 1
      Emby.Server.Implementations/Localization/Core/uk.json
  86. 4 2
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  87. 3 1
      Emby.Server.Implementations/Localization/Core/zh-TW.json
  88. 20 17
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  89. 26 13
      Emby.Server.Implementations/Net/SocketFactory.cs
  90. 1 1
      Emby.Server.Implementations/Playlists/PlaylistsFolder.cs
  91. 13 5
      Emby.Server.Implementations/Plugins/PluginManager.cs
  92. 2 2
      Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs
  93. 90 7
      Emby.Server.Implementations/Session/SessionManager.cs
  94. 2 1
      Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs
  95. 2 1
      Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs
  96. 2 1
      Emby.Server.Implementations/Sorting/AlbumComparer.cs
  97. 2 1
      Emby.Server.Implementations/Sorting/ArtistComparer.cs
  98. 2 1
      Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs
  99. 2 1
      Emby.Server.Implementations/Sorting/CriticRatingComparer.cs
  100. 2 1
      Emby.Server.Implementations/Sorting/DateCreatedComparer.cs

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

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

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

@@ -20,18 +20,18 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+      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@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
+      uses: github/codeql-action/init@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
       with:
         languages: ${{ matrix.language }}
         queries: +security-extended
     - name: Autobuild
-      uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
+      uses: github/codeql-action/autobuild@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
+      uses: github/codeql-action/analyze@74483a38d39275f33fcff5f35b679b5ca4a26a99 # v2.22.5

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

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

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

@@ -14,7 +14,7 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -39,7 +39,7 @@ jobs:
     permissions: read-all
     steps:
       - name: Checkout repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ github.event.pull_request.head.sha }}
           repository: ${{ github.event.pull_request.head.repo.full_name }}
@@ -112,7 +112,7 @@ jobs:
           direction: last
           body-includes: openapi-diff-workflow-comment
       - name: Reply or edit difference comment (changed)
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ steps.read-diff.outputs.body != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}
@@ -127,7 +127,7 @@ jobs:
 
             </details>
       - name: Edit difference comment (unchanged)
-        uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2
+        uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
         if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
         with:
           issue-number: ${{ github.event.pull_request.number }}

+ 2 - 2
.github/workflows/repo-bump-version.yaml

@@ -33,7 +33,7 @@ jobs:
           yq-version: v4.9.8
 
       - name: Checkout Repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ env.TAG_BRANCH }}
 
@@ -66,7 +66,7 @@ jobs:
       NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
     steps:
       - name: Checkout Repository
-        uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           ref: ${{ env.TAG_BRANCH }}
 

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

@@ -2,7 +2,7 @@ name: Stale Check
 
 on:
   schedule:
-    - cron: '30 */12 * * *'
+    - cron: '30 1 * * *'
   workflow_dispatch:
 
 permissions:
@@ -19,11 +19,12 @@ jobs:
       - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
+          ascending: true
           days-before-stale: 120
           days-before-pr-stale: -1
           days-before-close: 21
           days-before-pr-close: -1
-          operations-per-run: 75
+          operations-per-run: 500
           exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
           stale-issue-label: stale
           stale-issue-message: |-
@@ -41,7 +42,8 @@ jobs:
       - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
         with:
           repo-token: ${{ secrets.JF_BOT_TOKEN }}
-          operations-per-run: 75
+          ascending: true
+          operations-per-run: 150
           # The merge conflict action will remove the label when updated
           remove-stale-when-updated: false
           days-before-stale: -1

+ 3 - 0
CONTRIBUTORS.md

@@ -57,6 +57,7 @@
  - [hawken93](https://github.com/hawken93)
  - [HelloWorld017](https://github.com/HelloWorld017)
  - [ikomhoog](https://github.com/ikomhoog)
+ - [iwalton3](https://github.com/iwalton3)
  - [jftuga](https://github.com/jftuga)
  - [jmshrv](https://github.com/jmshrv)
  - [joern-h](https://github.com/joern-h)
@@ -88,6 +89,7 @@
  - [neilsb](https://github.com/neilsb)
  - [nevado](https://github.com/nevado)
  - [Nickbert7](https://github.com/Nickbert7)
+ - [nicknsy](https://github.com/nicknsy)
  - [nvllsvm](https://github.com/nvllsvm)
  - [nyanmisaka](https://github.com/nyanmisaka)
  - [OancaAndrei](https://github.com/OancaAndrei)
@@ -168,6 +170,7 @@
  - [TheTyrius](https://github.com/TheTyrius)
  - [tallbl0nde](https://github.com/tallbl0nde)
  - [sleepycatcoding](https://github.com/sleepycatcoding)
+ - [scampower3](https://github.com/scampower3)
 
 # Emby Contributors
 

+ 20 - 22
Directory.Packages.props

@@ -2,9 +2,7 @@
   <PropertyGroup>
     <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
   </PropertyGroup>
-
   <!-- Run "dotnet list package (dash,dash)outdated" to see the latest versions of each package.-->
-
   <ItemGroup Label="Package Dependencies">
     <PackageVersion Include="AutoFixture.AutoMoq" Version="4.18.0" />
     <PackageVersion Include="AutoFixture.Xunit2" Version="4.18.0" />
@@ -17,23 +15,23 @@
     <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="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" />
     <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="IDisposableAnalyzers" Version="4.0.4" />
     <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.12" />
+    <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" />
+    <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.13" />
     <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
-    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.13" />
     <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
-    <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
-    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.13" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.13" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.13" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.13" />
+    <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.13" />
     <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" />
@@ -42,14 +40,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.11" />
-    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.13" />
+    <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.13" />
     <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.7.2" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
     <PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
     <PackageVersion Include="MimeTypes" Version="2.4.0" />
     <PackageVersion Include="Mono.Nat" Version="3.0.4" />
@@ -57,14 +55,14 @@
     <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.1" />
+    <PackageVersion Include="prometheus-net.AspNetCore" Version="8.1.0" />
     <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
-    <PackageVersion Include="prometheus-net" Version="8.0.1" />
+    <PackageVersion Include="prometheus-net" Version="8.1.0" />
     <PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
     <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
     <PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
     <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
-    <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
+    <PackageVersion Include="Serilog.Sinks.Console" Version="5.0.0" />
     <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
     <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
     <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
@@ -72,9 +70,9 @@
     <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="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
     <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
+    <PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
     <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" />
@@ -86,8 +84,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.5.1" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
     <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
-    <PackageVersion Include="xunit" Version="2.5.1" />
+    <PackageVersion Include="xunit" Version="2.6.1" />
   </ItemGroup>
-</Project>
+</Project>

+ 20 - 32
Emby.Dlna/ContentDirectory/ControlHandler.cs

@@ -565,30 +565,18 @@ namespace Emby.Dlna.ContentDirectory
 
             if (stubType != StubType.Folder && item is IHasCollectionType collectionFolder)
             {
-                var collectionType = collectionFolder.CollectionType;
-                if (string.Equals(CollectionType.Music, collectionType, StringComparison.OrdinalIgnoreCase))
+                switch (collectionFolder.CollectionType)
                 {
-                    return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
-                }
-
-                if (string.Equals(CollectionType.Movies, collectionType, StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
-                }
-
-                if (string.Equals(CollectionType.TvShows, collectionType, StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetTvFolders(item, user, stubType, sort, startIndex, limit);
-                }
-
-                if (string.Equals(CollectionType.Folders, collectionType, StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetFolders(user, startIndex, limit);
-                }
-
-                if (string.Equals(CollectionType.LiveTv, collectionType, StringComparison.OrdinalIgnoreCase))
-                {
-                    return GetLiveTvChannels(user, sort, startIndex, limit);
+                    case CollectionType.Music:
+                        return GetMusicFolders(item, user, stubType, sort, startIndex, limit);
+                    case CollectionType.Movies:
+                        return GetMovieFolders(item, user, stubType, sort, startIndex, limit);
+                    case CollectionType.TvShows:
+                        return GetTvFolders(item, user, stubType, sort, startIndex, limit);
+                    case CollectionType.Folders:
+                        return GetFolders(user, startIndex, limit);
+                    case CollectionType.LiveTv:
+                        return GetLiveTvChannels(user, sort, startIndex, limit);
                 }
             }
 
@@ -917,7 +905,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetGenres(BaseItem parent, InternalItemsQuery query)
         {
             // Don't sort
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
             query.AncestorIds = new[] { parent.Id };
             var genresResult = _libraryManager.GetGenres(query);
 
@@ -933,7 +921,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicGenres(BaseItem parent, InternalItemsQuery query)
         {
             // Don't sort
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
             query.AncestorIds = new[] { parent.Id };
             var genresResult = _libraryManager.GetMusicGenres(query);
 
@@ -949,7 +937,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicAlbumArtists(BaseItem parent, InternalItemsQuery query)
         {
             // Don't sort
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
             query.AncestorIds = new[] { parent.Id };
             var artists = _libraryManager.GetAlbumArtists(query);
 
@@ -965,7 +953,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetMusicArtists(BaseItem parent, InternalItemsQuery query)
         {
             // Don't sort
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
             query.AncestorIds = new[] { parent.Id };
             var artists = _libraryManager.GetArtists(query);
             return ToResult(query.StartIndex, artists);
@@ -980,7 +968,7 @@ namespace Emby.Dlna.ContentDirectory
         private QueryResult<ServerItem> GetFavoriteArtists(BaseItem parent, InternalItemsQuery query)
         {
             // Don't sort
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
             query.AncestorIds = new[] { parent.Id };
             query.IsFavorite = true;
             var artists = _libraryManager.GetArtists(query);
@@ -1011,7 +999,7 @@ namespace Emby.Dlna.ContentDirectory
         /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
         private QueryResult<ServerItem> GetNextUp(BaseItem parent, InternalItemsQuery query)
         {
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
 
             var result = _tvSeriesManager.GetNextUp(
                 new NextUpQuery
@@ -1036,7 +1024,7 @@ namespace Emby.Dlna.ContentDirectory
         /// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
         private QueryResult<ServerItem> GetLatest(BaseItem parent, InternalItemsQuery query, BaseItemKind itemType)
         {
-            query.OrderBy = Array.Empty<(string, SortOrder)>();
+            query.OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
 
             var items = _userViewManager.GetLatestItems(
                 new LatestItemsQuery
@@ -1203,9 +1191,9 @@ namespace Emby.Dlna.ContentDirectory
         /// </summary>
         /// <param name="sort">The <see cref="SortCriteria"/>.</param>
         /// <param name="isPreSorted">True if pre-sorted.</param>
-        private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
+        private static (ItemSortBy SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
         {
-            return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
+            return isPreSorted ? Array.Empty<(ItemSortBy, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
         }
 
         /// <summary>

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

@@ -26,8 +26,12 @@
     <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
   </PropertyGroup>
 
-  <!-- Code Analyzers-->
+  <!-- Code Analyzers -->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="IDisposableAnalyzers">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 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
+        });
+    }
+}

+ 17 - 117
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -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,36 +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)
             {
@@ -232,19 +160,6 @@ 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 (_publisher is not null)
@@ -317,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[]
@@ -338,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))
             {
@@ -354,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 deviceTypeNamespace = serviceParts[0].Replace('.', '-');
+            var serviceParts = fullDeviceType
+                .Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
+                .Split(':');
 
-            device.DeviceTypeNamespace = deviceTypeNamespace;
+            device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
             device.DeviceClass = serviceParts[1];
             device.DeviceType = serviceParts[2];
         }
@@ -443,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;
         }
     }

+ 6 - 11
Emby.Dlna/PlayTo/Device.cs

@@ -927,14 +927,11 @@ 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];
@@ -1139,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
             return new Device(deviceProperties, httpClientFactory, logger);
         }
 
-#nullable enable
         private static DeviceIcon CreateIcon(XElement element)
         {
             ArgumentNullException.ThrowIfNull(element);
@@ -1252,11 +1248,10 @@ namespace Emby.Dlna.PlayTo
             if (disposing)
             {
                 _timer?.Dispose();
+                _timer = null;
+                Properties = null!;
             }
 
-            _timer = null;
-            Properties = null!;
-
             _disposed = true;
         }
 

+ 28 - 27
Emby.Dlna/PlayTo/DlnaHttpClient.cs

@@ -55,41 +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);
-            ms.Position = 0;
-            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;
+                    }
                 }
             }
         }

+ 6 - 7
Emby.Dlna/PlayTo/PlayToController.cs

@@ -684,16 +684,15 @@ namespace Emby.Dlna.PlayTo
 
             if (disposing)
             {
+                _device.PlaybackStart -= OnDevicePlaybackStart;
+                _device.PlaybackProgress -= OnDevicePlaybackProgress;
+                _device.PlaybackStopped -= OnDevicePlaybackStopped;
+                _device.MediaChanged -= OnDeviceMediaChanged;
+                _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
+                _device.OnDeviceUnavailable = null;
                 _device.Dispose();
             }
 
-            _device.PlaybackStart -= OnDevicePlaybackStart;
-            _device.PlaybackProgress -= OnDevicePlaybackProgress;
-            _device.PlaybackStopped -= OnDevicePlaybackStopped;
-            _device.MediaChanged -= OnDeviceMediaChanged;
-            _deviceDiscovery.DeviceLeft -= OnDeviceDiscoveryDeviceLeft;
-            _device.OnDeviceUnavailable = null;
-
             _disposed = true;
         }
 

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

@@ -39,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)
         {

+ 5 - 5
Emby.Naming/Common/NamingOptions.cs

@@ -318,7 +318,7 @@ 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[]
                     {
@@ -328,7 +328,7 @@ namespace Emby.Naming.Common
                         "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[]
                     {
@@ -376,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
                 },
@@ -417,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?[^\\\/]*$")
@@ -712,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.

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

@@ -45,8 +45,12 @@
     <PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
   </ItemGroup>
 
-  <!-- Code Analyzers-->
+  <!-- Code Analyzers -->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="IDisposableAnalyzers">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 6 - 2
Emby.Photos/Emby.Photos.csproj

@@ -24,14 +24,18 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
 
-  <!-- Code Analyzers-->
+  <!-- Code Analyzers -->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <PackageReference Include="IDisposableAnalyzers">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference>
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>
-    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
     <PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
+    <PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
     <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
   </ItemGroup>
 

+ 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%";

+ 3 - 11
Emby.Server.Implementations/ApplicationHost.cs

@@ -13,9 +13,7 @@ using System.Net;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
 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;
@@ -58,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;
@@ -82,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;
@@ -450,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)
@@ -563,8 +559,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
-
             serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
             serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@@ -576,8 +570,6 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
-
             serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
             serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@@ -913,7 +905,7 @@ namespace Emby.Server.Implementations
         /// <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;
@@ -948,7 +940,7 @@ namespace Emby.Server.Implementations
         public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
         {
             // With an empty source, the port will be null
-            var smart = NetManager.GetBindAddress(ipAddress, out _, true);
+            var smart = NetManager.GetBindAddress(ipAddress, out _, false);
             var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
             int? port = !allowHttps ? HttpPort : null;
             return GetLocalApiUrl(smart, scheme, port);

+ 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)

+ 43 - 169
Emby.Server.Implementations/Data/SqliteItemRepository.cs

@@ -2042,7 +2042,7 @@ namespace Emby.Server.Implementations.Data
                 return false;
             }
 
-            var sortingFields = new HashSet<string>(query.OrderBy.Select(i => i.OrderBy), StringComparer.OrdinalIgnoreCase);
+            var sortingFields = new HashSet<ItemSortBy>(query.OrderBy.Select(i => i.OrderBy));
 
             return sortingFields.Contains(ItemSortBy.IsFavoriteOrLiked)
                     || sortingFields.Contains(ItemSortBy.IsPlayed)
@@ -2832,20 +2832,20 @@ namespace Emby.Server.Implementations.Data
 
             if (hasSimilar || hasSearch)
             {
-                List<(string, SortOrder)> prepend = new List<(string, SortOrder)>(4);
+                List<(ItemSortBy, SortOrder)> prepend = new List<(ItemSortBy, SortOrder)>(4);
                 if (hasSearch)
                 {
-                    prepend.Add(("SearchScore", SortOrder.Descending));
+                    prepend.Add((ItemSortBy.SearchScore, SortOrder.Descending));
                     prepend.Add((ItemSortBy.SortName, SortOrder.Ascending));
                 }
 
                 if (hasSimilar)
                 {
-                    prepend.Add(("SimilarityScore", SortOrder.Descending));
+                    prepend.Add((ItemSortBy.SimilarityScore, SortOrder.Descending));
                     prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
                 }
 
-                var arr = new (string, SortOrder)[prepend.Count + orderBy.Count];
+                var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
                 prepend.CopyTo(arr, 0);
                 orderBy.CopyTo(arr, prepend.Count);
                 orderBy = query.OrderBy = arr;
@@ -2863,166 +2863,43 @@ namespace Emby.Server.Implementations.Data
             }));
         }
 
-        private string MapOrderByField(string name, InternalItemsQuery query)
+        private string MapOrderByField(ItemSortBy sortBy, InternalItemsQuery query)
         {
-            if (string.Equals(name, ItemSortBy.AirTime, StringComparison.OrdinalIgnoreCase))
-            {
-                // TODO
-                return "SortName";
-            }
-
-            if (string.Equals(name, ItemSortBy.Runtime, StringComparison.OrdinalIgnoreCase))
-            {
-                return "RuntimeTicks";
-            }
-
-            if (string.Equals(name, ItemSortBy.Random, StringComparison.OrdinalIgnoreCase))
-            {
-                return "RANDOM()";
-            }
-
-            if (string.Equals(name, ItemSortBy.DatePlayed, StringComparison.OrdinalIgnoreCase))
-            {
-                if (query.GroupBySeriesPresentationUniqueKey)
-                {
-                    return "MAX(LastPlayedDate)";
-                }
-
-                return "LastPlayedDate";
-            }
-
-            if (string.Equals(name, ItemSortBy.PlayCount, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.PlayCount;
-            }
-
-            if (string.Equals(name, ItemSortBy.IsFavoriteOrLiked, StringComparison.OrdinalIgnoreCase))
-            {
-                return "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )";
-            }
-
-            if (string.Equals(name, ItemSortBy.IsFolder, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.IsFolder;
-            }
-
-            if (string.Equals(name, ItemSortBy.IsPlayed, StringComparison.OrdinalIgnoreCase))
-            {
-                return "played";
-            }
-
-            if (string.Equals(name, ItemSortBy.IsUnplayed, StringComparison.OrdinalIgnoreCase))
-            {
-                return "played";
-            }
-
-            if (string.Equals(name, ItemSortBy.DateLastContentAdded, StringComparison.OrdinalIgnoreCase))
-            {
-                return "DateLastMediaAdded";
-            }
-
-            if (string.Equals(name, ItemSortBy.Artist, StringComparison.OrdinalIgnoreCase))
-            {
-                return "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)";
-            }
-
-            if (string.Equals(name, ItemSortBy.AlbumArtist, StringComparison.OrdinalIgnoreCase))
-            {
-                return "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)";
-            }
-
-            if (string.Equals(name, ItemSortBy.OfficialRating, StringComparison.OrdinalIgnoreCase))
-            {
-                return "InheritedParentalRatingValue";
-            }
-
-            if (string.Equals(name, ItemSortBy.Studio, StringComparison.OrdinalIgnoreCase))
-            {
-                return "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)";
-            }
-
-            if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase))
-            {
-                return "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)";
-            }
-
-            if (string.Equals(name, ItemSortBy.SeriesSortName, StringComparison.OrdinalIgnoreCase))
-            {
-                return "SeriesName";
-            }
-
-            if (string.Equals(name, ItemSortBy.AiredEpisodeOrder, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.AiredEpisodeOrder;
-            }
-
-            if (string.Equals(name, ItemSortBy.Album, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.Album;
-            }
-
-            if (string.Equals(name, ItemSortBy.DateCreated, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.DateCreated;
-            }
-
-            if (string.Equals(name, ItemSortBy.PremiereDate, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.PremiereDate;
-            }
-
-            if (string.Equals(name, ItemSortBy.StartDate, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.StartDate;
-            }
-
-            if (string.Equals(name, ItemSortBy.Name, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.Name;
-            }
-
-            if (string.Equals(name, ItemSortBy.CommunityRating, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.CommunityRating;
-            }
-
-            if (string.Equals(name, ItemSortBy.ProductionYear, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.ProductionYear;
-            }
-
-            if (string.Equals(name, ItemSortBy.CriticRating, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.CriticRating;
-            }
-
-            if (string.Equals(name, ItemSortBy.VideoBitRate, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.VideoBitRate;
-            }
-
-            if (string.Equals(name, ItemSortBy.ParentIndexNumber, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.ParentIndexNumber;
-            }
-
-            if (string.Equals(name, ItemSortBy.IndexNumber, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.IndexNumber;
-            }
-
-            if (string.Equals(name, ItemSortBy.SimilarityScore, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.SimilarityScore;
-            }
-
-            if (string.Equals(name, ItemSortBy.SearchScore, StringComparison.OrdinalIgnoreCase))
-            {
-                return ItemSortBy.SearchScore;
-            }
-
-            // Unknown SortBy, just sort by the SortName.
-            return ItemSortBy.SortName;
+            return sortBy switch
+            {
+                ItemSortBy.AirTime => "SortName", // TODO
+                ItemSortBy.Runtime => "RuntimeTicks",
+                ItemSortBy.Random => "RANDOM()",
+                ItemSortBy.DatePlayed when query.GroupBySeriesPresentationUniqueKey => "MAX(LastPlayedDate)",
+                ItemSortBy.DatePlayed => "LastPlayedDate",
+                ItemSortBy.PlayCount => "PlayCount",
+                ItemSortBy.IsFavoriteOrLiked => "(Select Case When IsFavorite is null Then 0 Else IsFavorite End )",
+                ItemSortBy.IsFolder => "IsFolder",
+                ItemSortBy.IsPlayed => "played",
+                ItemSortBy.IsUnplayed => "played",
+                ItemSortBy.DateLastContentAdded => "DateLastMediaAdded",
+                ItemSortBy.Artist => "(select CleanValue from ItemValues where ItemId=Guid and Type=0 LIMIT 1)",
+                ItemSortBy.AlbumArtist => "(select CleanValue from ItemValues where ItemId=Guid and Type=1 LIMIT 1)",
+                ItemSortBy.OfficialRating => "InheritedParentalRatingValue",
+                ItemSortBy.Studio => "(select CleanValue from ItemValues where ItemId=Guid and Type=3 LIMIT 1)",
+                ItemSortBy.SeriesDatePlayed => "(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)",
+                ItemSortBy.SeriesSortName => "SeriesName",
+                ItemSortBy.AiredEpisodeOrder => "AiredEpisodeOrder",
+                ItemSortBy.Album => "Album",
+                ItemSortBy.DateCreated => "DateCreated",
+                ItemSortBy.PremiereDate => "PremiereDate",
+                ItemSortBy.StartDate => "StartDate",
+                ItemSortBy.Name => "Name",
+                ItemSortBy.CommunityRating => "CommunityRating",
+                ItemSortBy.ProductionYear => "ProductionYear",
+                ItemSortBy.CriticRating => "CriticRating",
+                ItemSortBy.VideoBitRate => "VideoBitRate",
+                ItemSortBy.ParentIndexNumber => "ParentIndexNumber",
+                ItemSortBy.IndexNumber => "IndexNumber",
+                ItemSortBy.SimilarityScore => "SimilarityScore",
+                ItemSortBy.SearchScore => "SearchScore",
+                _ => "SortName"
+            };
         }
 
         public List<Guid> GetItemIdsList(InternalItemsQuery query)
@@ -3535,10 +3412,7 @@ namespace Emby.Server.Implementations.Data
                         .Append(paramName)
                         .Append("))) OR ");
 
-                    if (statement is not null)
-                    {
-                        statement.TryBind(paramName, query.PersonIds[i]);
-                    }
+                    statement?.TryBind(paramName, query.PersonIds[i]);
                 }
 
                 clauseBuilder.Length -= Or.Length;
@@ -4376,7 +4250,7 @@ namespace Emby.Server.Implementations.Data
 
                 foreach (var videoType in query.VideoTypes)
                 {
-                    videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'");
+                    videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
                 }
 
                 whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");

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

@@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
 using MediaBrowser.Controller.Persistence;
 using MediaBrowser.Controller.Playlists;
 using MediaBrowser.Controller.Providers;
+using MediaBrowser.Controller.Trickplay;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Querying;
@@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
         private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
 
         private readonly ILyricManager _lyricManager;
+        private readonly ITrickplayManager _trickplayManager;
 
         public DtoService(
             ILogger<DtoService> logger,
@@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
             IApplicationHost appHost,
             IMediaSourceManager mediaSourceManager,
             Lazy<ILiveTvManager> livetvManagerFactory,
-            ILyricManager lyricManager)
+            ILyricManager lyricManager,
+            ITrickplayManager trickplayManager)
         {
             _logger = logger;
             _libraryManager = libraryManager;
@@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
             _mediaSourceManager = mediaSourceManager;
             _livetvManagerFactory = livetvManagerFactory;
             _lyricManager = lyricManager;
+            _trickplayManager = trickplayManager;
         }
 
         private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
                     dto.Chapters = _itemRepo.GetChapters(item);
                 }
 
+                if (options.ContainsField(ItemFields.Trickplay))
+                {
+                    dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
+                }
+
                 if (video.ExtraType.HasValue)
                 {
                     dto.ExtraType = video.ExtraType.Value.ToString();

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

@@ -43,16 +43,19 @@
     <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' ">
     <CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
   </PropertyGroup>
 
-  <!-- Code Analyzers-->
+  <!-- Code Analyzers -->
   <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <!-- TODO: Add IDisposableAnalyzers -->
+    <!-- <PackageReference Include="IDisposableAnalyzers">
+      <PrivateAssets>all</PrivateAssets>
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+    </PackageReference> -->
     <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

+ 22 - 13
Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs

@@ -18,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
     {
@@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IConfiguration _config;
         private readonly IConfigurationManager _configurationManager;
         private readonly INetworkManager _networkManager;
-        private readonly bool _enableMultiSocketBinding;
 
         /// <summary>
         /// The UDP server.
         /// </summary>
-        private List<UdpServer> _udpServers;
-        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.
@@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
             _configurationManager = configurationManager;
             _networkManager = networkManager;
             _udpServers = new List<UdpServer>();
-            _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
         }
 
         /// <inheritdoc />
@@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
 
             try
             {
-                if (_enableMultiSocketBinding)
+                // 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 socket
+                    // 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 sockets
+                    // 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)
@@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
                 }
                 else
                 {
-                    var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, 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 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)
@@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
         {
             if (_disposed)
             {
-                throw new ObjectDisposedException(this.GetType().Name);
+                throw new ObjectDisposedException(GetType().Name);
             }
         }
 

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

@@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
 using MediaBrowser.Controller.Net.WebSocketMessages;
 using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
 using MediaBrowser.Model.Session;
-using Microsoft.AspNetCore.Http;
 using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.HttpServer

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

@@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
 
             DisposeTimer();
             _disposed = true;
-            GC.SuppressFinalize(this);
         }
     }
 }

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

@@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
             }
 
             // unc path
-            if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
+            if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
             {
                 return filePath;
             }

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

@@ -34,7 +34,7 @@ namespace Emby.Server.Implementations.Images
                 Recursive = true,
                 DtoOptions = new DtoOptions(true),
                 ImageTypes = new ImageType[] { ImageType.Primary },
-                OrderBy = new (string, SortOrder)[]
+                OrderBy = new (ItemSortBy, SortOrder)[]
                 {
                     (ItemSortBy.IsFolder, SortOrder.Ascending),
                     (ItemSortBy.SortName, SortOrder.Ascending)

+ 28 - 32
Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs

@@ -30,47 +30,43 @@ namespace Emby.Server.Implementations.Images
 
             BaseItemKind[] includeItemTypes;
 
-            if (string.Equals(viewType, CollectionType.Movies, StringComparison.Ordinal))
+            switch (viewType)
             {
-                includeItemTypes = new[] { BaseItemKind.Movie };
-            }
-            else if (string.Equals(viewType, CollectionType.TvShows, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.Series };
-            }
-            else if (string.Equals(viewType, CollectionType.Music, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.MusicAlbum };
-            }
-            else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.MusicVideo };
-            }
-            else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
-            }
-            else if (string.Equals(viewType, CollectionType.BoxSets, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.BoxSet };
-            }
-            else if (string.Equals(viewType, CollectionType.HomeVideos, StringComparison.Ordinal) || string.Equals(viewType, CollectionType.Photos, StringComparison.Ordinal))
-            {
-                includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
-            }
-            else
-            {
-                includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+                case CollectionType.Movies:
+                    includeItemTypes = new[] { BaseItemKind.Movie };
+                    break;
+                case CollectionType.TvShows:
+                    includeItemTypes = new[] { BaseItemKind.Series };
+                    break;
+                case CollectionType.Music:
+                    includeItemTypes = new[] { BaseItemKind.MusicAlbum };
+                    break;
+                case CollectionType.MusicVideos:
+                    includeItemTypes = new[] { BaseItemKind.MusicVideo };
+                    break;
+                case CollectionType.Books:
+                    includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
+                    break;
+                case CollectionType.BoxSets:
+                    includeItemTypes = new[] { BaseItemKind.BoxSet };
+                    break;
+                case CollectionType.HomeVideos:
+                case CollectionType.Photos:
+                    includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
+                    break;
+                default:
+                    includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Audio, BaseItemKind.Photo, BaseItemKind.Movie, BaseItemKind.Series };
+                    break;
             }
 
-            var recursive = !string.Equals(CollectionType.Playlists, viewType, StringComparison.OrdinalIgnoreCase);
+            var recursive = viewType != CollectionType.Playlists;
 
             return view.GetItemList(new InternalItemsQuery
             {
                 CollapseBoxSetItems = false,
                 Recursive = recursive,
                 DtoOptions = new DtoOptions(false),
-                ImageTypes = new ImageType[] { ImageType.Primary },
+                ImageTypes = new[] { ImageType.Primary },
                 Limit = 8,
                 OrderBy = new[]
                 {

+ 3 - 3
Emby.Server.Implementations/Images/DynamicImageProvider.cs

@@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
             var view = (UserView)item;
 
             var isUsingCollectionStrip = IsUsingCollectionStrip(view);
-            var recursive = isUsingCollectionStrip && !new[] { CollectionType.BoxSets, CollectionType.Playlists }.Contains(view.ViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase);
+            var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists;
 
             var result = view.GetItemList(new InternalItemsQuery
             {
@@ -112,14 +112,14 @@ namespace Emby.Server.Implementations.Images
 
         private static bool IsUsingCollectionStrip(UserView view)
         {
-            string[] collectionStripViewTypes =
+            CollectionType[] collectionStripViewTypes =
             {
                 CollectionType.Movies,
                 CollectionType.TvShows,
                 CollectionType.Playlists
             };
 
-            return collectionStripViewTypes.Contains(view.ViewType ?? string.Empty);
+            return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);
         }
 
         protected override string CreateImage(BaseItem item, IReadOnlyCollection<BaseItem> itemsWithImages, string outputPathWithoutExtension, ImageType imageType, int imageIndex)

+ 42 - 37
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -525,14 +525,14 @@ namespace Emby.Server.Implementations.Library
             IDirectoryService directoryService,
             IItemResolver[] resolvers,
             Folder parent = null,
-            string collectionType = null,
+            CollectionType? collectionType = null,
             LibraryOptions libraryOptions = null)
         {
             ArgumentNullException.ThrowIfNull(fileInfo);
 
             var fullPath = fileInfo.FullName;
 
-            if (string.IsNullOrEmpty(collectionType) && parent is not null)
+            if (collectionType is null && parent is not null)
             {
                 collectionType = GetContentTypeOverride(fullPath, true);
             }
@@ -635,7 +635,7 @@ namespace Emby.Server.Implementations.Library
             return !args.ContainsFileSystemEntryByName(".ignore");
         }
 
-        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, string collectionType = null)
+        public IEnumerable<BaseItem> ResolvePaths(IEnumerable<FileSystemMetadata> files, IDirectoryService directoryService, Folder parent, LibraryOptions libraryOptions, CollectionType? collectionType = null)
         {
             return ResolvePaths(files, directoryService, parent, libraryOptions, collectionType, EntityResolvers);
         }
@@ -645,7 +645,7 @@ namespace Emby.Server.Implementations.Library
             IDirectoryService directoryService,
             Folder parent,
             LibraryOptions libraryOptions,
-            string collectionType,
+            CollectionType? collectionType,
             IItemResolver[] resolvers)
         {
             var fileList = files.Where(i => !IgnoreFile(i, parent)).ToList();
@@ -675,7 +675,7 @@ namespace Emby.Server.Implementations.Library
             IReadOnlyList<FileSystemMetadata> fileList,
             IDirectoryService directoryService,
             Folder parent,
-            string collectionType,
+            CollectionType? collectionType,
             IItemResolver[] resolvers,
             LibraryOptions libraryOptions)
         {
@@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
         {
             if (item is UserView view)
             {
-                if (string.Equals(view.ViewType, CollectionType.LiveTv, StringComparison.Ordinal))
+                if (view.ViewType == CollectionType.LiveTv)
                 {
                     return new[] { view.Id };
                 }
@@ -1543,13 +1543,13 @@ namespace Emby.Server.Implementations.Library
                 }
 
                 // Handle grouping
-                if (user is not null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
+                if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType)
                     && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
                 {
                     return GetUserRootFolder()
                         .GetChildren(user, true)
                         .OfType<CollectionFolder>()
-                        .Where(i => string.IsNullOrEmpty(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase))
+                        .Where(i => i.CollectionType is null || i.CollectionType == view.ViewType)
                         .Where(i => user.IsFolderGrouped(i.Id))
                         .SelectMany(i => GetTopParentIdsForQuery(i, user));
                 }
@@ -1678,7 +1678,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="sortBy">The sort by.</param>
         /// <param name="sortOrder">The sort order.</param>
         /// <returns>IEnumerable{BaseItem}.</returns>
-        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<string> sortBy, SortOrder sortOrder)
+        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<ItemSortBy> sortBy, SortOrder sortOrder)
         {
             var isFirst = true;
 
@@ -1701,7 +1701,7 @@ namespace Emby.Server.Implementations.Library
             return orderedItems ?? items;
         }
 
-        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(string OrderBy, SortOrder SortOrder)> orderBy)
+        public IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user, IEnumerable<(ItemSortBy OrderBy, SortOrder SortOrder)> orderBy)
         {
             var isFirst = true;
 
@@ -1736,9 +1736,9 @@ namespace Emby.Server.Implementations.Library
         /// <param name="name">The name.</param>
         /// <param name="user">The user.</param>
         /// <returns>IBaseItemComparer.</returns>
-        private IBaseItemComparer GetComparer(string name, User user)
+        private IBaseItemComparer GetComparer(ItemSortBy name, User user)
         {
-            var comparer = Comparers.FirstOrDefault(c => string.Equals(name, c.Name, StringComparison.OrdinalIgnoreCase));
+            var comparer = Comparers.FirstOrDefault(c => name == c.Type);
 
             // If it requires a user, create a new one, and assign the user
             if (comparer is IUserBaseItemComparer)
@@ -2065,16 +2065,16 @@ namespace Emby.Server.Implementations.Library
                 : collectionFolder.GetLibraryOptions();
         }
 
-        public string GetContentType(BaseItem item)
+        public CollectionType? GetContentType(BaseItem item)
         {
-            string configuredContentType = GetConfiguredContentType(item, false);
-            if (!string.IsNullOrEmpty(configuredContentType))
+            var configuredContentType = GetConfiguredContentType(item, false);
+            if (configuredContentType is not null)
             {
                 return configuredContentType;
             }
 
             configuredContentType = GetConfiguredContentType(item, true);
-            if (!string.IsNullOrEmpty(configuredContentType))
+            if (configuredContentType is not null)
             {
                 return configuredContentType;
             }
@@ -2082,31 +2082,31 @@ namespace Emby.Server.Implementations.Library
             return GetInheritedContentType(item);
         }
 
-        public string GetInheritedContentType(BaseItem item)
+        public CollectionType? GetInheritedContentType(BaseItem item)
         {
             var type = GetTopFolderContentType(item);
 
-            if (!string.IsNullOrEmpty(type))
+            if (type is not null)
             {
                 return type;
             }
 
             return item.GetParents()
                 .Select(GetConfiguredContentType)
-                .LastOrDefault(i => !string.IsNullOrEmpty(i));
+                .LastOrDefault(i => i is not null);
         }
 
-        public string GetConfiguredContentType(BaseItem item)
+        public CollectionType? GetConfiguredContentType(BaseItem item)
         {
             return GetConfiguredContentType(item, false);
         }
 
-        public string GetConfiguredContentType(string path)
+        public CollectionType? GetConfiguredContentType(string path)
         {
             return GetContentTypeOverride(path, false);
         }
 
-        public string GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
+        public CollectionType? GetConfiguredContentType(BaseItem item, bool inheritConfiguredPath)
         {
             if (item is ICollectionFolder collectionFolder)
             {
@@ -2116,16 +2116,21 @@ namespace Emby.Server.Implementations.Library
             return GetContentTypeOverride(item.ContainingFolderPath, inheritConfiguredPath);
         }
 
-        private string GetContentTypeOverride(string path, bool inherit)
+        private CollectionType? GetContentTypeOverride(string path, bool inherit)
         {
             var nameValuePair = _configurationManager.Configuration.ContentTypes
                                     .FirstOrDefault(i => _fileSystem.AreEqual(i.Name, path)
                                                          || (inherit && !string.IsNullOrEmpty(i.Name)
                                                                      && _fileSystem.ContainsSubPath(i.Name, path)));
-            return nameValuePair?.Value;
+            if (Enum.TryParse<CollectionType>(nameValuePair?.Value, out var collectionType))
+            {
+                return collectionType;
+            }
+
+            return null;
         }
 
-        private string GetTopFolderContentType(BaseItem item)
+        private CollectionType? GetTopFolderContentType(BaseItem item)
         {
             if (item is null)
             {
@@ -2147,13 +2152,13 @@ namespace Emby.Server.Implementations.Library
                 .OfType<ICollectionFolder>()
                 .Where(i => string.Equals(i.Path, item.Path, StringComparison.OrdinalIgnoreCase) || i.PhysicalLocations.Contains(item.Path))
                 .Select(i => i.CollectionType)
-                .FirstOrDefault(i => !string.IsNullOrEmpty(i));
+                .FirstOrDefault(i => i is not null);
         }
 
         public UserView GetNamedView(
             User user,
             string name,
-            string viewType,
+            CollectionType? viewType,
             string sortName)
         {
             return GetNamedView(user, name, Guid.Empty, viewType, sortName);
@@ -2161,13 +2166,13 @@ namespace Emby.Server.Implementations.Library
 
         public UserView GetNamedView(
             string name,
-            string viewType,
+            CollectionType viewType,
             string sortName)
         {
             var path = Path.Combine(
                 _configurationManager.ApplicationPaths.InternalMetadataPath,
                 "views",
-                _fileSystem.GetValidFilename(viewType));
+                _fileSystem.GetValidFilename(viewType.ToString()));
 
             var id = GetNewItemId(path + "_namedview_" + name, typeof(UserView));
 
@@ -2207,13 +2212,13 @@ namespace Emby.Server.Implementations.Library
             User user,
             string name,
             Guid parentId,
-            string viewType,
+            CollectionType? viewType,
             string sortName)
         {
             var parentIdString = parentId.Equals(default)
                 ? null
                 : parentId.ToString("N", CultureInfo.InvariantCulture);
-            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+            var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
 
             var id = GetNewItemId(idValues, typeof(UserView));
 
@@ -2269,7 +2274,7 @@ namespace Emby.Server.Implementations.Library
 
         public UserView GetShadowView(
             BaseItem parent,
-            string viewType,
+            CollectionType? viewType,
             string sortName)
         {
             ArgumentNullException.ThrowIfNull(parent);
@@ -2277,7 +2282,7 @@ namespace Emby.Server.Implementations.Library
             var name = parent.Name;
             var parentId = parent.Id;
 
-            var idValues = "38_namedview_" + name + parentId + (viewType ?? string.Empty);
+            var idValues = "38_namedview_" + name + parentId + (viewType?.ToString() ?? string.Empty);
 
             var id = GetNewItemId(idValues, typeof(UserView));
 
@@ -2334,7 +2339,7 @@ namespace Emby.Server.Implementations.Library
         public UserView GetNamedView(
             string name,
             Guid parentId,
-            string viewType,
+            CollectionType? viewType,
             string sortName,
             string uniqueId)
         {
@@ -2343,7 +2348,7 @@ namespace Emby.Server.Implementations.Library
             var parentIdString = parentId.Equals(default)
                 ? null
                 : parentId.ToString("N", CultureInfo.InvariantCulture);
-            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty);
+            var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty);
             if (!string.IsNullOrEmpty(uniqueId))
             {
                 idValues += uniqueId;
@@ -2378,7 +2383,7 @@ namespace Emby.Server.Implementations.Library
                 isNew = true;
             }
 
-            if (!string.Equals(viewType, item.ViewType, StringComparison.OrdinalIgnoreCase))
+            if (viewType != item.ViewType)
             {
                 item.ViewType = viewType;
                 item.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, CancellationToken.None).GetAwaiter().GetResult();
@@ -2850,7 +2855,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);

+ 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

@@ -626,17 +626,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)
@@ -665,8 +667,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);
                 }

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

@@ -10,11 +10,11 @@ using Emby.Naming.Audio;
 using Emby.Naming.AudioBook;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
 using MediaBrowser.Controller.Resolvers;
-using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.IO;
 
 namespace Emby.Server.Implementations.Library.Resolvers.Audio
@@ -40,7 +40,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         public MultiItemResolverResult ResolveMultiple(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType,
+            CollectionType? collectionType,
             IDirectoryService directoryService)
         {
             var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -59,9 +59,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         private MultiItemResolverResult ResolveMultipleInternal(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType)
+            CollectionType? collectionType)
         {
-            if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+            if (collectionType == CollectionType.Books)
             {
                 return ResolveMultipleAudio(parent, files, true);
             }
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var collectionType = args.GetCollectionType();
 
-            var isBooksCollectionType = string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase);
+            var isBooksCollectionType = collectionType == CollectionType.Books;
 
             if (args.IsDirectory)
             {
@@ -102,7 +102,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                     return null;
                 }
 
-                var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
+                var isMixedCollectionType = collectionType is null;
 
                 // For conflicting extensions, give priority to videos
                 if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
                 MediaBrowser.Controller.Entities.Audio.Audio item = null;
 
-                var isMusicCollectionType = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+                var isMusicCollectionType = collectionType == CollectionType.Music;
 
                 // Use regular audio type for mixed libraries, owned items and music
                 if (isMixedCollectionType ||

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

@@ -8,6 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Emby.Naming.Audio;
 using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -54,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         protected override MusicAlbum Resolve(ItemResolveArgs args)
         {
             var collectionType = args.GetCollectionType();
-            var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+            var isMusicMediaFolder = collectionType == CollectionType.Music;
 
             // If there's a collection type and it's not music, don't allow it.
             if (!isMusicMediaFolder)

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs

@@ -4,6 +4,7 @@ using System;
 using System.Linq;
 using System.Threading.Tasks;
 using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var collectionType = args.GetCollectionType();
 
-            var isMusicMediaFolder = string.Equals(collectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase);
+            var isMusicMediaFolder = collectionType == CollectionType.Music;
 
             // If there's a collection type and it's not music, it can't be a music artist
             if (!isMusicMediaFolder)

+ 2 - 1
Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs

@@ -5,6 +5,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -22,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
             var collectionType = args.GetCollectionType();
 
             // Only process items that are in a collection folder containing books
-            if (!string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
+            if (collectionType != CollectionType.Books)
             {
                 return null;
             }

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

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Text.RegularExpressions;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
@@ -28,13 +29,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
     {
         private readonly IImageProcessor _imageProcessor;
 
-        private string[] _validCollectionTypes = new[]
+        private static readonly CollectionType[] _validCollectionTypes = new[]
         {
-                CollectionType.Movies,
-                CollectionType.HomeVideos,
-                CollectionType.MusicVideos,
-                CollectionType.TvShows,
-                CollectionType.Photos
+            CollectionType.Movies,
+            CollectionType.HomeVideos,
+            CollectionType.MusicVideos,
+            CollectionType.TvShows,
+            CollectionType.Photos
         };
 
         /// <summary>
@@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         public MultiItemResolverResult ResolveMultiple(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType,
+            CollectionType? collectionType,
             IDirectoryService directoryService)
         {
             var result = ResolveMultipleInternal(parent, files, collectionType);
@@ -99,17 +100,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 Video movie = null;
                 var files = args.GetActualFileSystemChildren().ToList();
 
-                if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+                if (collectionType == CollectionType.MusicVideos)
                 {
                     movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
                 }
 
-                if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase))
+                if (collectionType == CollectionType.HomeVideos)
                 {
                     movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false);
                 }
 
-                if (string.IsNullOrEmpty(collectionType))
+                if (collectionType is null)
                 {
                     // Owned items will be caught by the video extra resolver
                     if (args.Parent is null)
@@ -125,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
                 }
 
-                if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+                if (collectionType == CollectionType.Movies)
                 {
                     movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true);
                 }
@@ -146,22 +147,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
 
             Video item = null;
 
-            if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+            if (collectionType == CollectionType.MusicVideos)
             {
                 item = ResolveVideo<MusicVideo>(args, false);
             }
 
             // To find a movie file, the collection type must be movies or boxsets
-            else if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+            else if (collectionType == CollectionType.Movies)
             {
                 item = ResolveVideo<Movie>(args, true);
             }
-            else if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
-                string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+            else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
             {
                 item = ResolveVideo<Video>(args, false);
             }
-            else if (string.IsNullOrEmpty(collectionType))
+            else if (collectionType is null)
             {
                 if (args.HasParent<Series>())
                 {
@@ -188,25 +188,24 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         private MultiItemResolverResult ResolveMultipleInternal(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType)
+            CollectionType? collectionType)
         {
             if (IsInvalid(parent, collectionType))
             {
                 return null;
             }
 
-            if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
+            if (collectionType is CollectionType.MusicVideos)
             {
                 return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false);
             }
 
-            if (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) ||
-                            string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase))
+            if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos)
             {
                 return ResolveVideos<Video>(parent, files, false, collectionType, false);
             }
 
-            if (string.IsNullOrEmpty(collectionType))
+            if (collectionType is null)
             {
                 // Owned items should just use the plain video type
                 if (parent is null)
@@ -222,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 return ResolveVideos<Movie>(parent, files, false, collectionType, true);
             }
 
-            if (string.Equals(collectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase))
+            if (collectionType == CollectionType.Movies)
             {
                 return ResolveVideos<Movie>(parent, files, true, collectionType, true);
             }
 
-            if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+            if (collectionType == CollectionType.TvShows)
             {
                 return ResolveVideos<Episode>(parent, files, false, collectionType, true);
             }
@@ -239,13 +238,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             Folder parent,
             IEnumerable<FileSystemMetadata> fileSystemEntries,
             bool supportMultiEditions,
-            string collectionType,
+            CollectionType? collectionType,
             bool parseName)
             where T : Video, new()
         {
             var files = new List<FileSystemMetadata>();
             var leftOver = new List<FileSystemMetadata>();
-            var hasCollectionType = !string.IsNullOrEmpty(collectionType);
+            var hasCollectionType = collectionType is not null;
 
             // Loop through each child file/folder and see if we find a video
             foreach (var child in fileSystemEntries)
@@ -398,13 +397,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         /// Finds a movie based on a child file system entries.
         /// </summary>
         /// <returns>Movie.</returns>
-        private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
+        private T FindMovie<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, CollectionType? collectionType, bool parseName)
             where T : Video, new()
         {
             var multiDiscFolders = new List<FileSystemMetadata>();
 
             var libraryOptions = args.LibraryOptions;
-            var supportPhotos = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && libraryOptions.EnablePhotos;
+            var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos;
             var photos = new List<FileSystemMetadata>();
 
             // Search for a folder rip
@@ -460,8 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
                 new MultiItemResolverResult();
 
-            var isPhotosCollection = string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase)
-                                         || string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase);
+            var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos;
             if (!isPhotosCollection && result.Items.Count == 1)
             {
                 var videoPath = result.Items[0].Path;
@@ -562,7 +560,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             return returnVideo;
         }
 
-        private bool IsInvalid(Folder parent, ReadOnlySpan<char> collectionType)
+        private bool IsInvalid(Folder parent, CollectionType? collectionType)
         {
             if (parent is not null)
             {
@@ -572,12 +570,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
             }
 
-            if (collectionType.IsEmpty)
+            if (collectionType is null)
             {
                 return false;
             }
 
-            return !_validCollectionTypes.Contains(collectionType, StringComparison.OrdinalIgnoreCase);
+            return !_validCollectionTypes.Contains(collectionType.Value);
         }
     }
 }

+ 3 - 2
Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs

@@ -2,6 +2,7 @@
 
 using System;
 using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -45,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 // Must be an image file within a photo collection
                 var collectionType = args.GetCollectionType();
 
-                if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
-                    || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+                if (collectionType == CollectionType.Photos
+                    || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
                 {
                     if (HasPhotos(args))
                     {

+ 3 - 3
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -1,9 +1,9 @@
 using System;
-using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using Emby.Naming.Common;
 using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
@@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                 // Must be an image file within a photo collection
                 var collectionType = args.CollectionType;
 
-                if (string.Equals(collectionType, CollectionType.Photos, StringComparison.OrdinalIgnoreCase)
-                    || (string.Equals(collectionType, CollectionType.HomeVideos, StringComparison.OrdinalIgnoreCase) && args.LibraryOptions.EnablePhotos))
+                if (collectionType == CollectionType.Photos
+                    || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos))
                 {
                     if (IsImageFile(args.Path, _imageProcessor))
                     {

+ 3 - 3
Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs

@@ -20,9 +20,9 @@ namespace Emby.Server.Implementations.Library.Resolvers
     /// </summary>
     public class PlaylistResolver : GenericFolderResolver<Playlist>
     {
-        private string[] _musicPlaylistCollectionTypes =
+        private CollectionType?[] _musicPlaylistCollectionTypes =
         {
-            string.Empty,
+            null,
             CollectionType.Music
         };
 
@@ -63,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
             // Check if this is a music playlist file
             // It should have the correct collection type and a supported file extension
-            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+            else if (_musicPlaylistCollectionTypes.Contains(args.CollectionType))
             {
                 var extension = Path.GetExtension(args.Path.AsSpan());
                 if (Playlist.SupportedExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))

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

@@ -5,6 +5,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -62,7 +63,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
             return null;
         }
 
-        private string GetCollectionType(ItemResolveArgs args)
+        private CollectionType? GetCollectionType(ItemResolveArgs args)
         {
             return args.FileSystemChildren
                 .Where(i =>
@@ -78,7 +79,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     }
                 })
                 .Select(i => _fileSystem.GetFileNameWithoutExtension(i))
-                .FirstOrDefault();
+                .Select(i => Enum.TryParse<CollectionType>(i, out var collectionType) ? collectionType : (CollectionType?)null)
+                .FirstOrDefault(i => i is not null);
         }
     }
 }

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

@@ -3,6 +3,7 @@
 using System;
 using System.Linq;
 using Emby.Naming.Common;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -48,9 +49,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
 
             // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something
             // Also handle flat tv folders
-            if (season is not null ||
-                string.Equals(args.GetCollectionType(), CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ||
-                args.HasParent<Series>())
+            if (season is not null
+                || args.GetCollectionType() == CollectionType.TvShows
+                || args.HasParent<Series>())
             {
                 var episode = ResolveVideo<Episode>(args, false);
 

+ 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);
 

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

@@ -8,6 +8,7 @@ using System.IO;
 using Emby.Naming.Common;
 using Emby.Naming.TV;
 using Emby.Naming.Video;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
@@ -59,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
 
                 var collectionType = args.GetCollectionType();
-                if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+                if (collectionType == CollectionType.TvShows)
                 {
                     // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
                     var configuredContentType = args.GetConfiguredContentType();
-                    if (!string.Equals(configuredContentType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
+                    if (configuredContentType != CollectionType.TvShows)
                     {
                         return new Series
                         {
@@ -72,7 +73,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                         };
                     }
                 }
-                else if (string.IsNullOrEmpty(collectionType))
+                else if (collectionType is null)
                 {
                     if (args.ContainsFileSystemEntryByName("tvshow.nfo"))
                     {

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

@@ -8,7 +8,6 @@ using System.Linq;
 using System.Threading;
 using Jellyfin.Data.Entities;
 using Jellyfin.Data.Enums;
-using Jellyfin.Extensions;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dto;
@@ -64,8 +63,8 @@ namespace Emby.Server.Implementations.Library
                 var collectionFolder = folder as ICollectionFolder;
                 var folderViewType = collectionFolder?.CollectionType;
 
-                // Playlist library requires special handling because the folder only refrences user playlists
-                if (string.Equals(folderViewType, CollectionType.Playlists, StringComparison.OrdinalIgnoreCase))
+                // Playlist library requires special handling because the folder only references user playlists
+                if (folderViewType == CollectionType.Playlists)
                 {
                     var items = folder.GetItemList(new InternalItemsQuery(user)
                     {
@@ -90,7 +89,7 @@ namespace Emby.Server.Implementations.Library
                     continue;
                 }
 
-                if (query.PresetViews.Contains(folderViewType ?? string.Empty, StringComparison.OrdinalIgnoreCase))
+                if (query.PresetViews.Contains(folderViewType))
                 {
                     list.Add(GetUserView(folder, folderViewType, string.Empty));
                 }
@@ -102,14 +101,14 @@ namespace Emby.Server.Implementations.Library
 
             foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows })
             {
-                var parents = groupedFolders.Where(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(i.CollectionType))
+                var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
                     .ToList();
 
                 if (parents.Count > 0)
                 {
-                    var localizationKey = string.Equals(viewType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase) ?
-                        "TvShows" :
-                        "Movies";
+                    var localizationKey = viewType == CollectionType.TvShows
+                        ? "TvShows"
+                        : "Movies";
 
                     list.Add(GetUserView(parents, viewType, localizationKey, string.Empty, user, query.PresetViews));
                 }
@@ -164,14 +163,14 @@ namespace Emby.Server.Implementations.Library
                 .ToArray();
         }
 
-        public UserView GetUserSubViewWithName(string name, Guid parentId, string type, string sortName)
+        public UserView GetUserSubViewWithName(string name, Guid parentId, CollectionType? type, string sortName)
         {
             var uniqueId = parentId + "subview" + type;
 
             return _libraryManager.GetNamedView(name, parentId, type, sortName, uniqueId);
         }
 
-        public UserView GetUserSubView(Guid parentId, string type, string localizationKey, string sortName)
+        public UserView GetUserSubView(Guid parentId, CollectionType? type, string localizationKey, string sortName)
         {
             var name = _localizationManager.GetLocalizedString(localizationKey);
 
@@ -180,15 +179,15 @@ namespace Emby.Server.Implementations.Library
 
         private Folder GetUserView(
             List<ICollectionFolder> parents,
-            string viewType,
+            CollectionType? viewType,
             string localizationKey,
             string sortName,
             User user,
-            string[] presetViews)
+            CollectionType?[] presetViews)
         {
-            if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
+            if (parents.Count == 1 && parents.All(i => i.CollectionType == viewType))
             {
-                if (!presetViews.Contains(viewType, StringComparison.OrdinalIgnoreCase))
+                if (!presetViews.Contains(viewType))
                 {
                     return (Folder)parents[0];
                 }
@@ -200,7 +199,7 @@ namespace Emby.Server.Implementations.Library
             return _libraryManager.GetNamedView(user, name, viewType, sortName);
         }
 
-        public UserView GetUserView(Folder parent, string viewType, string sortName)
+        public UserView GetUserView(Folder parent, CollectionType? viewType, string sortName)
         {
             return _libraryManager.GetShadowView(parent, viewType, sortName);
         }
@@ -280,7 +279,7 @@ namespace Emby.Server.Implementations.Library
 
             var isPlayed = request.IsPlayed;
 
-            if (parents.OfType<ICollectionFolder>().Any(i => string.Equals(i.CollectionType, CollectionType.Music, StringComparison.OrdinalIgnoreCase)))
+            if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music))
             {
                 isPlayed = null;
             }
@@ -306,11 +305,11 @@ namespace Emby.Server.Implementations.Library
                 var hasCollectionType = parents.OfType<UserView>().ToArray();
                 if (hasCollectionType.Length > 0)
                 {
-                    if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.Movies, StringComparison.OrdinalIgnoreCase)))
+                    if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies))
                     {
                         includeItemTypes = new[] { BaseItemKind.Movie };
                     }
-                    else if (hasCollectionType.All(i => string.Equals(i.CollectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)))
+                    else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows))
                     {
                         includeItemTypes = new[] { BaseItemKind.Episode };
                     }

+ 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>();

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

@@ -207,7 +207,7 @@ namespace Emby.Server.Implementations.LiveTv
                 orderBy.Insert(0, (ItemSortBy.IsFavoriteOrLiked, SortOrder.Descending));
             }
 
-            if (!internalQuery.OrderBy.Any(i => string.Equals(i.OrderBy, ItemSortBy.SortName, StringComparison.OrdinalIgnoreCase)))
+            if (internalQuery.OrderBy.All(i => i.OrderBy != ItemSortBy.SortName))
             {
                 orderBy.Add((ItemSortBy.SortName, SortOrder.Ascending));
             }

+ 0 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs

@@ -17,7 +17,6 @@ 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

+ 30 - 32
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
@@ -76,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();
@@ -129,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))
                 {
@@ -175,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
+                        });
+                    }
                 }
             }
 

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

@@ -44,8 +44,6 @@ 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)

+ 0 - 2
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;
 

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

@@ -123,5 +123,7 @@
     "TaskCleanTranscodeDescription": "Выдаляе перакадзіраваныя файлы, старэйшыя за адзін дзень.",
     "TaskRefreshChannels": "Абнавіць каналы",
     "TaskDownloadMissingSubtitles": "Спампаваць адсутныя субтытры",
-    "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу."
+    "TaskKeyframeExtractorDescription": "Выдае ключавыя кадры з відэафайлаў для стварэння больш дакладных спісаў прайгравання HLS. Гэта задача можа працаваць у працягу доўгага часу.",
+    "TaskRefreshTrickplayImages": "Стварыце выявы Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Стварае прагляд відэаролікаў для Trickplay у падключаных бібліятэках."
 }

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

@@ -124,5 +124,6 @@
     "TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
     "TaskKeyframeExtractor": "Извличане на ключови кадри",
     "External": "Външен",
-    "HearingImpaired": "Увреден слух"
+    "HearingImpaired": "Увреден слух",
+    "TaskRefreshTrickplayImages": "Генерирай изображение"
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.",
     "TaskKeyframeExtractor": "Vytahovač klíčových snímků",
     "External": "Externí",
-    "HearingImpaired": "Sluchově postižení"
+    "HearingImpaired": "Sluchově postižení",
+    "TaskRefreshTrickplayImages": "Generovat obrázky pro Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Obrázky Trickplay se používají k zobrazení náhledů u videí v knihovnách, kde je to povoleno."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.",
     "TaskKeyframeExtractor": "Keyframe Extraktor",
     "External": "Extern",
-    "HearingImpaired": "Hörgeschädigt"
+    "HearingImpaired": "Hörgeschädigt",
+    "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren",
+    "TaskRefreshTrickplayImagesDescription": "Erstellt eine Trickplay-Vorschau für Videos in aktivierten Bibliotheken."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Εξάγει καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς λίστες αναπαραγωγής HLS. Αυτή η διεργασία μπορεί να πάρει χρόνο.",
     "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο",
     "External": "Εξωτερικό",
-    "HearingImpaired": "Με προβλήματα ακοής"
+    "HearingImpaired": "Με προβλήματα ακοής",
+    "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.",
     "TaskKeyframeExtractor": "Keyframe Extractor",
     "External": "External",
-    "HearingImpaired": "Hearing Impaired"
+    "HearingImpaired": "Hearing Impaired",
+    "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+    "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries."
 }

+ 2 - 0
Emby.Server.Implementations/Localization/Core/en-US.json

@@ -112,6 +112,8 @@
     "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
     "TaskRefreshPeople": "Refresh People",
     "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
+    "TaskRefreshTrickplayImages": "Generate Trickplay Images",
+    "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
     "TaskUpdatePlugins": "Update Plugins",
     "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
     "TaskCleanTranscode": "Clean Transcode Directory",

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.",
     "TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
     "External": "Externo",
-    "HearingImpaired": "Discapacidad Auditiva"
+    "HearingImpaired": "Discapacidad Auditiva",
+    "TaskRefreshTrickplayImages": "Generar miniaturas de línea de tiempo",
+    "TaskRefreshTrickplayImagesDescription": "Crear miniaturas de tiempo para videos en las librerías habilitadas."
 }

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

@@ -123,5 +123,7 @@
     "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.",
     "TaskKeyframeExtractor": "Avainkuvien purkain",
     "External": "Ulkoinen",
-    "HearingImpaired": "Kuulorajoitteinen"
+    "HearingImpaired": "Kuulorajoitteinen",
+    "TaskRefreshTrickplayImages": "Luo Trickplay-kuvat",
+    "TaskRefreshTrickplayImagesDescription": "Luo Trickplay-esikatselut käytössä olevien kirjastojen videoista."
 }

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

@@ -123,5 +123,6 @@
     "HearingImpaired": "Bingi",
     "TaskKeyframeExtractor": "Tagabunot ng Keyframe",
     "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
-    "External": "External"
+    "External": "External",
+    "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe"
 }

+ 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"
+}

+ 8 - 6
Emby.Server.Implementations/Localization/Core/fr.json

@@ -5,7 +5,7 @@
     "Artists": "Artistes",
     "AuthenticationSucceededWithUserName": "{0} authentifié avec succès",
     "Books": "Livres",
-    "CameraImageUploadedFrom": "Une photo a été téléversée depuis {0}",
+    "CameraImageUploadedFrom": "Une photo a été téléchargée depuis {0}",
     "Channels": "Chaînes",
     "ChapterNameValue": "Chapitre {0}",
     "Collections": "Collections",
@@ -16,14 +16,14 @@
     "Folders": "Dossiers",
     "Genres": "Genres",
     "HeaderAlbumArtists": "Artistes de l'album",
-    "HeaderContinueWatching": "Reprendre le visionnage",
+    "HeaderContinueWatching": "Continuer de regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes préférés",
     "HeaderFavoriteEpisodes": "Épisodes favoris",
     "HeaderFavoriteShows": "Séries favorites",
     "HeaderFavoriteSongs": "Chansons préférées",
     "HeaderLiveTV": "TV en direct",
-    "HeaderNextUp": "À suivre",
+    "HeaderNextUp": "Prochain à venir",
     "HeaderRecordingGroups": "Groupes d'enregistrements",
     "HomeVideos": "Vidéos personnelles",
     "Inherit": "Hériter",
@@ -71,7 +71,7 @@
     "ScheduledTaskStartedWithName": "{0} a démarré",
     "ServerNameNeedsToBeRestarted": "{0} doit être redémarré",
     "Shows": "Séries",
-    "Songs": "Titres",
+    "Songs": "Chansons",
     "StartupEmbyServerIsLoading": "Le serveur Jellyfin est en cours de chargement. Veuillez réessayer dans quelques instants.",
     "SubtitleDownloadFailureForItem": "Le téléchargement des sous-titres pour {0} a échoué.",
     "SubtitleDownloadFailureFromForItem": "Échec du téléchargement des sous-titres depuis {0} pour {1}",
@@ -122,7 +122,9 @@
     "TaskOptimizeDatabaseDescription": "Réduit les espaces vides ou inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la médiathèque ou toute autre modification de la base de données peut améliorer les performances du serveur.",
     "TaskOptimizeDatabase": "Optimiser la base de données",
     "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.",
-    "TaskKeyframeExtractor": "Extracteur d'image clé",
+    "TaskKeyframeExtractor": "Extracteur d'images clés",
     "External": "Externe",
-    "HearingImpaired": "Malentendants"
+    "HearingImpaired": "Malentendants",
+    "TaskRefreshTrickplayImages": "Générer des images Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
 }

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

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

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Izvlačenje ključnih okvira iz videozapisa za stvaranje objektivnije HLS liste za reprodukciju. Pokretanje ovog zadatka može potrajati.",
     "TaskKeyframeExtractor": "Izvoditelj ključnog okvira",
     "TaskOptimizeDatabaseDescription": "Sažima bazu podataka i uklanja prazan prostor. Pokretanje ovog zadatka, može poboljšati performanse nakon provođenja indeksiranja biblioteke ili provođenja drugih promjena koje utječu na bazu podataka.",
-    "HearingImpaired": "Oštećen sluh"
+    "HearingImpaired": "Oštećen sluh",
+    "TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
+    "TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama."
 }

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

@@ -124,5 +124,7 @@
     "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"
+    "HearingImpaired": "Hallássérült",
+    "TaskRefreshTrickplayImages": "Trickplay képek generálása",
+    "TaskRefreshTrickplayImagesDescription": "Trickplay előnézetet készít az engedélyezett könyvtárakban lévő videókhoz."
 }

+ 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"
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractor": "Estrattore di Keyframe",
     "TaskKeyframeExtractorDescription": "Estrae i keyframe dai video per creare migliori playlist HLS. Questa procedura potrebbe richiedere molto tempo.",
     "External": "Esterno",
-    "HearingImpaired": "con problemi di udito"
+    "HearingImpaired": "con problemi di udito",
+    "TaskRefreshTrickplayImages": "Genera immagini Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Crea anteprime trickplay per i video nelle librerie abilitate."
 }

+ 22 - 20
Emby.Server.Implementations/Localization/Core/ja.json

@@ -4,19 +4,19 @@
     "Application": "アプリケーション",
     "Artists": "アーティスト",
     "AuthenticationSucceededWithUserName": "{0} 認証に成功しました",
-    "Books": "ブック",
+    "Books": "ブック",
     "CameraImageUploadedFrom": "新しいカメライメージが {0}からアップロードされました",
     "Channels": "チャンネル",
     "ChapterNameValue": "チャプター {0}",
     "Collections": "コレクション",
-    "DeviceOfflineWithName": "{0} が切断されました",
-    "DeviceOnlineWithName": "{0} が接続されました",
-    "FailedLoginAttemptWithUserName": "ログインを試行しましたが {0} よって失敗しました",
+    "DeviceOfflineWithName": "{0} が切断ました",
+    "DeviceOnlineWithName": "{0} が接続ました",
+    "FailedLoginAttemptWithUserName": "{0} からのログインに失敗しました",
     "Favorites": "お気に入り",
     "Folders": "フォルダー",
     "Genres": "ジャンル",
     "HeaderAlbumArtists": "アルバムアーティスト",
-    "HeaderContinueWatching": "続けて見る",
+    "HeaderContinueWatching": "再生を続ける",
     "HeaderFavoriteAlbums": "お気に入りのアルバム",
     "HeaderFavoriteArtists": "お気に入りのアーティスト",
     "HeaderFavoriteEpisodes": "お気に入りのエピソード",
@@ -27,22 +27,22 @@
     "HeaderRecordingGroups": "レコーディンググループ",
     "HomeVideos": "ホームビデオ",
     "Inherit": "継承",
-    "ItemAddedWithName": "{0} をライブラリに追加しました",
-    "ItemRemovedWithName": "{0} をライブラリから削除しました",
+    "ItemAddedWithName": "{0} をライブラリに追加しました",
+    "ItemRemovedWithName": "{0} をライブラリから削除しました",
     "LabelIpAddressValue": "IPアドレス: {0}",
-    "LabelRunningTimeValue": "稼働時間: {0}",
+    "LabelRunningTimeValue": "時間: {0}",
     "Latest": "最新",
-    "MessageApplicationUpdated": "Jellyfin Server が更新されました",
-    "MessageApplicationUpdatedTo": "Jellyfin Server が {0}に更新されました",
-    "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} が更新されました",
-    "MessageServerConfigurationUpdated": "サーバー設定が更新されました",
+    "MessageApplicationUpdated": "Jellyfin Server を更新しました",
+    "MessageApplicationUpdatedTo": "Jellyfin Server を {0}に更新しました",
+    "MessageNamedServerConfigurationUpdatedWithValue": "サーバー設定項目の {0} を更新しました",
+    "MessageServerConfigurationUpdated": "サーバー設定を更新しました",
     "MixedContent": "ミックスコンテンツ",
     "Movies": "映画",
     "Music": "音楽",
     "MusicVideos": "ミュージックビデオ",
     "NameInstallFailed": "{0}のインストールに失敗しました",
     "NameSeasonNumber": "シーズン {0}",
-    "NameSeasonUnknown": "不明なシーズン",
+    "NameSeasonUnknown": "シーズン不明",
     "NewVersionIsAvailable": "新しいバージョンの Jellyfin Server がダウンロード可能です。",
     "NotificationOptionApplicationUpdateAvailable": "アプリケーションの更新があります",
     "NotificationOptionApplicationUpdateInstalled": "アプリケーションは最新です",
@@ -88,18 +88,18 @@
     "UserPolicyUpdatedWithName": "ユーザーポリシーが{0}に更新されました",
     "UserStartedPlayingItemWithValues": "{0} は {2}で{1} を再生しています",
     "UserStoppedPlayingItemWithValues": "{0} は{2}で{1} の再生が終わりました",
-    "ValueHasBeenAddedToLibrary": "{0}はあなたのメディアライブラリに追加されました",
+    "ValueHasBeenAddedToLibrary": "{0} をメディアライブラリーに追加しました",
     "ValueSpecialEpisodeName": "スペシャル - {0}",
     "VersionNumber": "バージョン {0}",
     "TaskCleanLogsDescription": "{0} 日以上前のログを消去します。",
     "TaskCleanLogs": "ログの掃除",
-    "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。",
-    "TaskRefreshLibrary": "メディアライブラリスキャン",
+    "TaskRefreshLibraryDescription": "メディアライブラリをスキャンして新しいファイルを探し、メタデータを更新します。",
+    "TaskRefreshLibrary": "メディアライブラリーをスキャン",
     "TaskCleanCacheDescription": "不要なキャッシュを消去します。",
     "TaskCleanCache": "キャッシュを消去",
     "TasksChannelsCategory": "ネットチャンネル",
     "TasksApplicationCategory": "アプリケーション",
-    "TasksLibraryCategory": "ライブラリ",
+    "TasksLibraryCategory": "ライブラリ",
     "TasksMaintenanceCategory": "メンテナンス",
     "TaskRefreshChannelsDescription": "ネットチャンネルの情報を更新する。",
     "TaskRefreshChannels": "チャンネルの更新",
@@ -107,7 +107,7 @@
     "TaskCleanTranscode": "トランスコードディレクトリの削除",
     "TaskUpdatePluginsDescription": "自動更新可能なプラグインのアップデートをダウンロードしてインストールします。",
     "TaskUpdatePlugins": "プラグインの更新",
-    "TaskRefreshPeopleDescription": "メディアライブラリ俳優や監督のメタデータを更新します。",
+    "TaskRefreshPeopleDescription": "メディアライブラリー内の俳優や監督のメタデータを更新します。",
     "TaskRefreshPeople": "俳優や監督のデータの更新",
     "TaskDownloadMissingSubtitlesDescription": "メタデータ構成に基づいて、欠落している字幕をインターネットで検索する。",
     "TaskRefreshChapterImagesDescription": "チャプターのあるビデオのサムネイルを作成します。",
@@ -118,10 +118,12 @@
     "Undefined": "未定義",
     "Forced": "強制",
     "Default": "デフォルト",
-    "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリのスキャン後でこのタスクを実行するとパフォーマンスが向上する可能性があります。",
+    "TaskOptimizeDatabaseDescription": "データベースをコンパクトにして、空き領域を切り詰めます。メディアライブラリーのスキャンやその他のデータベースの更新を伴う変更の後でこのタスクを実行すると、パフォーマンスが向上します。",
     "TaskOptimizeDatabase": "データベースの最適化",
     "TaskKeyframeExtractorDescription": "より正確なHLSプレイリストを作成するため、動画ファイルからキーフレームを抽出する。この処理には時間がかかる場合があります。",
     "TaskKeyframeExtractor": "キーフレーム抽出",
     "External": "外部",
-    "HearingImpaired": "聴覚障害の方"
+    "HearingImpaired": "聴覚障害の方",
+    "TaskRefreshTrickplayImages": "トリックプレー画像を生成",
+    "TaskRefreshTrickplayImagesDescription": "有効なライブラリ内のビデオをもとにトリックプレーのプレビューを生成します。"
 }

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

@@ -123,5 +123,8 @@
     "TaskOptimizeDatabase": "Derekqordy oñtailandyru",
     "TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.",
     "TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru",
-    "External": "Syrtqy"
+    "External": "Syrtqy",
+    "TaskRefreshTrickplayImagesDescription": "Іске қосылған кітапханалардағы бейнелер үшін Trickplay алдын ала түрінде көрсетілімді жасайды.",
+    "TaskRefreshTrickplayImages": "Trickplay үшін суреттерді жасау",
+    "HearingImpaired": "Есту қабілеті нашарға"
 }

+ 41 - 41
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,13 +14,13 @@
     "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",
     "UserStoppedPlayingItemWithValues": "{0} ir beidzis atskaņot {1} uz {2}",
     "UserStartedPlayingItemWithValues": "{0} atskaņo {1} uz {2}",
-    "UserPasswordChangedWithName": "Parole nomainīta lietotājam {0}",
+    "UserPasswordChangedWithName": "Lietotāja {0} parole tika nomainīta",
     "UserOnlineFromDevice": "{0} ir tiešsaistē no {1}",
     "UserOfflineFromDevice": "{0} ir atvienojies no {1}",
     "UserLockedOutWithName": "Lietotājs {0} ir ticis bloķēts",
@@ -28,23 +28,23 @@
     "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.",
     "Songs": "Dziesmas",
-    "Shows": "Raidījumi",
+    "Shows": "Šovi",
     "PluginUpdatedWithName": "{0} tika atjaunots",
     "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",
+    "NotificationOptionServerRestartRequired": "Nepieciešams servera restarts",
     "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
     "NotificationOptionPluginUninstalled": "Paplašinājums noņemts",
     "NotificationOptionPluginInstalled": "Paplašinā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": "Turpini skatīties",
+    "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/nl.json

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Haalt keyframes uit videobestanden om preciezere HLS-afspeellijsten te maken. Deze taak kan lang duren.",
     "TaskKeyframeExtractor": "Keyframe-uitpakker",
     "External": "Extern",
-    "HearingImpaired": "Slechthorend"
+    "HearingImpaired": "Slechthorend",
+    "TaskRefreshTrickplayImages": "Trickplay-afbeeldingen genereren",
+    "TaskRefreshTrickplayImagesDescription": "Genereert trickplay-afbeeldingen voor video's in bibliotheken waarvoor dit is ingeschakeld."
 }

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

@@ -124,5 +124,7 @@
     "External": "Zewnętrzny",
     "TaskKeyframeExtractorDescription": "Wyodrębnia klatki kluczowe z plików wideo w celu utworzenia bardziej precyzyjnych list odtwarzania HLS. To zadanie może trwać przez długi czas.",
     "TaskKeyframeExtractor": "Ekstraktor klatek kluczowych",
-    "HearingImpaired": "Niedosłyszący"
+    "HearingImpaired": "Niedosłyszący",
+    "TaskRefreshTrickplayImages": "Generuj obrazy trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Tworzy podglądy trickplay dla filmów we włączonych bibliotekach."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Extrai quadros-chave de ficheiros de video para criar listas de reprodução HLS mais precisas. Esta tarefa pode demorar algum tempo.",
     "TaskKeyframeExtractor": "Extrator de Quadros-chave",
     "External": "Externo",
-    "HearingImpaired": "Surdo"
+    "HearingImpaired": "Surdo",
+    "TaskRefreshTrickplayImages": "Gerar imagens de truques",
+    "TaskRefreshTrickplayImagesDescription": "Cria vizualizações de truques para videos nas librarias ativas."
 }

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

@@ -92,7 +92,7 @@
     "Application": "Aplicação",
     "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}",
     "TaskCleanCache": "Limpar Diretório de Cache",
-    "TasksApplicationCategory": "Aplicativo",
+    "TasksApplicationCategory": "Aplicação",
     "TasksLibraryCategory": "Biblioteca",
     "TasksMaintenanceCategory": "Manutenção",
     "TaskRefreshChannels": "Atualizar Canais",
@@ -123,5 +123,7 @@
     "External": "Externo",
     "HearingImpaired": "Problemas auditivos",
     "TaskKeyframeExtractor": "Extrator de quadro-chave",
-    "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo."
+    "TaskKeyframeExtractorDescription": "Retira frames chave do video para criar listas HLS precisas. Esta tarefa pode correr durante algum tempo.",
+    "TaskRefreshTrickplayImages": "Gerar miniaturas de vídeo",
+    "TaskRefreshTrickplayImagesDescription": "Cria miniaturas de vídeo para vídeos nas bibliotecas definidas."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.",
     "TaskKeyframeExtractor": "Извлечение ключевых кадров",
     "External": "Внешние",
-    "HearingImpaired": "Для слабослышащих"
+    "HearingImpaired": "Для слабослышащих",
+    "TaskRefreshTrickplayImages": "Сгенерировать изображения для Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Создает предпросмотры для Trickplay для видео в библиотеках, где эта функция включена."
 }

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

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

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

@@ -124,5 +124,7 @@
     "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í",
+    "TaskRefreshTrickplayImages": "Generovanie obrázkov Trickplay",
+    "TaskRefreshTrickplayImagesDescription": "Vytvára trickplay náhľady pre videá v povolených knižniciach."
 }

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

@@ -124,5 +124,7 @@
     "TaskKeyframeExtractorDescription": "Exporterar nyckelbildrutor från videofiler för att skapa mer exakta HLS-spellistor. Denna rutin kan ta lång tid.",
     "TaskKeyframeExtractor": "Extraktor för nyckelbildrutor",
     "External": "Extern",
-    "HearingImpaired": "Hörselskadad"
+    "HearingImpaired": "Hörselskadad",
+    "TaskRefreshTrickplayImages": "Generera Trickplay-bilder",
+    "TaskRefreshTrickplayImagesDescription": "Skapar trickplay-förhandsvisningar för videor i aktiverade bibliotek."
 }

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

@@ -102,7 +102,7 @@
     "SubtitleDownloadFailureFromForItem": "வசன வரிகள் {0} இல் இருந்து {1} க்கு பதிவிறக்கத் தவறிவிட்டன",
     "TaskDownloadMissingSubtitlesDescription": "மீத்தரவு உள்ளமைவின் அடிப்படையில் வசன வரிகள் காணாமல் போனதற்கு இணையத்தைத் தேடுகிறது.",
     "TaskCleanTranscodeDescription": "ஒரு நாளைக்கு மேற்பட்ட பழைய டிரான்ஸ்கோட் கோப்புகளை நீக்குகிறது.",
-    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்க கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்குகிறது மற்றும் நிறுவுகிறது.",
+    "TaskUpdatePluginsDescription": "தானாகவே புதுப்பிக்கும்படி கட்டமைக்கப்பட்ட உட்செருகிகளுக்கான புதுப்பிப்புகளை பதிவிறக்கி நிறுவுகிறது.",
     "TaskRefreshPeopleDescription": "உங்கள் ஊடக நூலகத்தில் உள்ள நடிகர்கள் மற்றும் இயக்குனர்களுக்கான மீத்தரவை புதுப்பிக்கும்.",
     "TaskCleanLogsDescription": "{0} நாட்களுக்கு மேல் இருக்கும் பதிவு கோப்புகளை நீக்கும்.",
     "TaskCleanLogs": "பதிவு அடைவை சுத்தம் செய்யுங்கள்",
@@ -123,5 +123,7 @@
     "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.",
     "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்",
     "External": "வெளி",
-    "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்"
+    "HearingImpaired": "செவித்திறன் குறைபாடுடையவர்",
+    "TaskRefreshTrickplayImages": "முன்னோட்ட படங்களை உருவாக்கு",
+    "TaskRefreshTrickplayImagesDescription": "செயல்பாட்டில் உள்ள தொகுப்புகளுக்கு முன்னோட்ட படங்களை உருவாக்கும்."
 }

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

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

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

@@ -121,8 +121,10 @@
     "Default": "默认",
     "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。",
     "TaskOptimizeDatabase": "优化数据库",
-    "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。",
+    "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的 HLS 播放列表。这项任务可能需要很长时间。",
     "TaskKeyframeExtractor": "关键帧提取器",
     "External": "外部",
-    "HearingImpaired": "听力障碍"
+    "HearingImpaired": "听力障碍",
+    "TaskRefreshTrickplayImages": "生成时间轴缩略图",
+    "TaskRefreshTrickplayImagesDescription": "为启用的媒体库中的视频生成时间轴缩略图。"
 }

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

@@ -123,5 +123,7 @@
     "TaskKeyframeExtractorDescription": "將關鍵幀從影片檔案提取出來並建立更精準的HLS播放清單。這可能需要很長時間。",
     "TaskKeyframeExtractor": "關鍵幀提取器",
     "External": "外部",
-    "HearingImpaired": "聽力障礙"
+    "HearingImpaired": "聽力障礙",
+    "TaskRefreshTrickplayImages": "生成快轉縮圖",
+    "TaskRefreshTrickplayImagesDescription": "為啟用此設定的媒體庫生成快轉縮圖。"
 }

+ 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);
+                        }
                     }
                 }
 

+ 26 - 13
Emby.Server.Implementations/Net/SocketFactory.cs

@@ -1,12 +1,15 @@
-#pragma warning disable CS1591
-
 using System;
+using System.Linq;
 using System.Net;
+using System.Net.NetworkInformation;
 using System.Net.Sockets;
 using MediaBrowser.Model.Net;
 
 namespace Emby.Server.Implementations.Net
 {
+    /// <summary>
+    /// Factory class to create different kinds of sockets.
+    /// </summary>
     public class SocketFactory : ISocketFactory
     {
         /// <inheritdoc />
@@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
             }
             catch
             {
-                socket?.Dispose();
+                socket.Dispose();
 
                 throw;
             }
@@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
         /// <inheritdoc />
         public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
         {
-            ArgumentNullException.ThrowIfNull(bindInterface.Address);
+            var interfaceAddress = bindInterface.Address;
+            ArgumentNullException.ThrowIfNull(interfaceAddress);
 
             if (localPort < 0)
             {
@@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
             try
             {
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
-                socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
+                socket.Bind(new IPEndPoint(interfaceAddress, localPort));
 
                 return socket;
             }
             catch
             {
-                socket?.Dispose();
+                socket.Dispose();
 
                 throw;
             }
@@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
 
             try
             {
-                var interfaceIndex = bindInterface.Index;
-                var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
-
                 socket.MulticastLoopback = false;
                 socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
                 socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
-                socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
-                socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
-                socket.Bind(new IPEndPoint(multicastAddress, localPort));
+
+                if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
+                {
+                    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
+                    socket.Bind(new IPEndPoint(multicastAddress, localPort));
+                }
+                else
+                {
+                    // Only create socket if interface supports multicast
+                    var interfaceIndex = bindInterface.Index;
+                    var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
+
+                    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
+                    socket.Bind(new IPEndPoint(bindIPAddress, localPort));
+                }
 
                 return socket;
             }
             catch
             {
-                socket?.Dispose();
+                socket.Dispose();
 
                 throw;
             }

+ 1 - 1
Emby.Server.Implementations/Playlists/PlaylistsFolder.cs

@@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists
         public override bool SupportsInheritedParentImages => false;
 
         [JsonIgnore]
-        public override string CollectionType => MediaBrowser.Model.Entities.CollectionType.Playlists;
+        public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists;
 
         protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
         {

+ 13 - 5
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
                 var url = new Uri(packageInfo.ImageUrl);
                 imagePath = Path.Join(path, url.Segments[^1]);
 
-                await using var fileStream = AsyncFile.OpenWrite(imagePath);
-
+                var fileStream = AsyncFile.OpenWrite(imagePath);
+                Stream? downloadStream = null;
                 try
                 {
-                    await using var downloadStream = await HttpClientFactory
+                    downloadStream = await HttpClientFactory
                         .CreateClient(NamedClient.Default)
                         .GetStreamAsync(url)
                         .ConfigureAwait(false);
@@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
                     _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
                     imagePath = string.Empty;
                 }
+                finally
+                {
+                    await fileStream.DisposeAsync().ConfigureAwait(false);
+                    if (downloadStream is not null)
+                    {
+                        await downloadStream.DisposeAsync().ConfigureAwait(false);
+                    }
+                }
             }
 
             var manifest = new PluginManifest
@@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
                 ImagePath = imagePath
             };
 
-            if (!await ReconcileManifest(manifest, path))
+            if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
             {
                 // An error occurred during reconciliation and saving could be undesirable.
                 return false;
@@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
                 }
 
                 using var metaStream = File.OpenRead(metafile);
-                var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions);
+                var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false);
                 localManifest ??= new PluginManifest();
 
                 if (!Equals(localManifest.Id, manifest.Id))

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

@@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
             {
                 try
                 {
-                    previouslyFailedImages = File.ReadAllText(failHistoryPath)
+                    previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
                         .Split('|', StringSplitOptions.RemoveEmptyEntries)
                         .ToList();
                 }
@@ -157,7 +157,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
                         }
 
                         string text = string.Join('|', previouslyFailedImages);
-                        File.WriteAllText(failHistoryPath, text);
+                        await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
                     }
 
                     numComplete++;

+ 90 - 7
Emby.Server.Implementations/Session/SessionManager.cs

@@ -19,6 +19,7 @@ using MediaBrowser.Common.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Authentication;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Devices;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Dto;
@@ -48,6 +49,7 @@ namespace Emby.Server.Implementations.Session
     public sealed class SessionManager : ISessionManager, IAsyncDisposable
     {
         private readonly IUserDataManager _userDataManager;
+        private readonly IServerConfigurationManager _config;
         private readonly ILogger<SessionManager> _logger;
         private readonly IEventManager _eventManager;
         private readonly ILibraryManager _libraryManager;
@@ -63,6 +65,7 @@ namespace Emby.Server.Implementations.Session
             = new(StringComparer.OrdinalIgnoreCase);
 
         private Timer _idleTimer;
+        private Timer _inactiveTimer;
 
         private DtoOptions _itemInfoDtoOptions;
         private bool _disposed = false;
@@ -71,6 +74,7 @@ namespace Emby.Server.Implementations.Session
             ILogger<SessionManager> logger,
             IEventManager eventManager,
             IUserDataManager userDataManager,
+            IServerConfigurationManager config,
             ILibraryManager libraryManager,
             IUserManager userManager,
             IMusicManager musicManager,
@@ -84,6 +88,7 @@ namespace Emby.Server.Implementations.Session
             _logger = logger;
             _eventManager = eventManager;
             _userDataManager = userDataManager;
+            _config = config;
             _libraryManager = libraryManager;
             _userManager = userManager;
             _musicManager = musicManager;
@@ -369,6 +374,15 @@ namespace Emby.Server.Implementations.Session
                 session.LastPlaybackCheckIn = DateTime.UtcNow;
             }
 
+            if (info.IsPaused && session.LastPausedDate is null)
+            {
+                session.LastPausedDate = DateTime.UtcNow;
+            }
+            else if (!info.IsPaused)
+            {
+                session.LastPausedDate = null;
+            }
+
             session.PlayState.IsPaused = info.IsPaused;
             session.PlayState.PositionTicks = info.PositionTicks;
             session.PlayState.MediaSourceId = info.MediaSourceId;
@@ -536,9 +550,18 @@ namespace Emby.Server.Implementations.Session
             return users;
         }
 
-        private void StartIdleCheckTimer()
+        private void StartCheckTimers()
         {
             _idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
+
+            if (_config.Configuration.InactiveSessionThreshold > 0)
+            {
+                _inactiveTimer ??= new Timer(CheckForInactiveSteams, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(1));
+            }
+            else
+            {
+                StopInactiveCheckTimer();
+            }
         }
 
         private void StopIdleCheckTimer()
@@ -550,6 +573,15 @@ namespace Emby.Server.Implementations.Session
             }
         }
 
+        private void StopInactiveCheckTimer()
+        {
+            if (_inactiveTimer is not null)
+            {
+                _inactiveTimer.Dispose();
+                _inactiveTimer = null;
+            }
+        }
+
         private async void CheckForIdlePlayback(object state)
         {
             var playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
@@ -585,13 +617,50 @@ namespace Emby.Server.Implementations.Session
                 playingSessions = Sessions.Where(i => i.NowPlayingItem is not null)
                     .ToList();
             }
-
-            if (playingSessions.Count == 0)
+            else
             {
                 StopIdleCheckTimer();
             }
         }
 
+        private async void CheckForInactiveSteams(object state)
+        {
+            var inactiveSessions = Sessions.Where(i =>
+                    i.NowPlayingItem is not null
+                    && i.PlayState.IsPaused
+                    && (DateTime.UtcNow - i.LastPausedDate).Value.TotalMinutes > _config.Configuration.InactiveSessionThreshold);
+
+            foreach (var session in inactiveSessions)
+            {
+                _logger.LogDebug("Session {Session} has been inactive for {InactiveTime} minutes. Stopping it.", session.Id, _config.Configuration.InactiveSessionThreshold);
+
+                try
+                {
+                    await SendPlaystateCommand(
+                        session.Id,
+                        session.Id,
+                        new PlaystateRequest()
+                        {
+                            Command = PlaystateCommand.Stop,
+                            ControllingUserId = session.UserId.ToString(),
+                            SeekPositionTicks = session.PlayState?.PositionTicks
+                        },
+                        CancellationToken.None).ConfigureAwait(true);
+                }
+                catch (Exception ex)
+                {
+                    _logger.LogDebug(ex, "Error calling SendPlaystateCommand for stopping inactive session {Session}.", session.Id);
+                }
+            }
+
+            bool playingSessions = Sessions.Any(i => i.NowPlayingItem is not null);
+
+            if (!playingSessions)
+            {
+                StopInactiveCheckTimer();
+            }
+        }
+
         private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId)
         {
             var item = session.FullNowPlayingItem;
@@ -668,7 +737,7 @@ namespace Emby.Server.Implementations.Session
                 eventArgs,
                 _logger);
 
-            StartIdleCheckTimer();
+            StartCheckTimers();
         }
 
         /// <summary>
@@ -762,7 +831,7 @@ namespace Emby.Server.Implementations.Session
                 session.StartAutomaticProgress(info);
             }
 
-            StartIdleCheckTimer();
+            StartCheckTimers();
         }
 
         private void OnPlaybackProgress(User user, BaseItem item, PlaybackProgressInfo info)
@@ -1384,10 +1453,15 @@ namespace Emby.Server.Implementations.Session
             return AuthenticateNewSessionInternal(request, false);
         }
 
-        private async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
+        internal async Task<AuthenticationResult> AuthenticateNewSessionInternal(AuthenticationRequest request, bool enforcePassword)
         {
             CheckDisposed();
 
+            ArgumentException.ThrowIfNullOrEmpty(request.App);
+            ArgumentException.ThrowIfNullOrEmpty(request.DeviceId);
+            ArgumentException.ThrowIfNullOrEmpty(request.DeviceName);
+            ArgumentException.ThrowIfNullOrEmpty(request.AppVersion);
+
             User user = null;
             if (!request.UserId.Equals(default))
             {
@@ -1448,8 +1522,11 @@ namespace Emby.Server.Implementations.Session
             return returnResult;
         }
 
-        private async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
+        internal async Task<string> GetAuthorizationToken(User user, string deviceId, string app, string appVersion, string deviceName)
         {
+            // This should be validated above, but if it isn't don't delete all tokens.
+            ArgumentException.ThrowIfNullOrEmpty(deviceId);
+
             var existing = (await _deviceManager.GetDevices(
                 new DeviceQuery
                 {
@@ -1798,6 +1875,12 @@ namespace Emby.Server.Implementations.Session
                 _idleTimer = null;
             }
 
+            if (_inactiveTimer is not null)
+            {
+                await _inactiveTimer.DisposeAsync().ConfigureAwait(false);
+                _inactiveTimer = null;
+            }
+
             await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
 
             _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;

+ 2 - 1
Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Sorting;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.AiredEpisodeOrder;
+        public ItemSortBy Type => ItemSortBy.AiredEpisodeOrder;
 
         /// <summary>
         /// Compares the specified x.

+ 2 - 1
Emby.Server.Implementations/Sorting/AlbumArtistComparer.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Sorting;
@@ -16,7 +17,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.AlbumArtist;
+        public ItemSortBy Type => ItemSortBy.AlbumArtist;
 
         /// <summary>
         /// Compares the specified x.

+ 2 - 1
Emby.Server.Implementations/Sorting/AlbumComparer.cs

@@ -1,4 +1,5 @@
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Sorting;
@@ -15,7 +16,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.Album;
+        public ItemSortBy Type => ItemSortBy.Album;
 
         /// <summary>
         /// Compares the specified x.

+ 2 - 1
Emby.Server.Implementations/Sorting/ArtistComparer.cs

@@ -1,4 +1,5 @@
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Sorting;
@@ -12,7 +13,7 @@ namespace Emby.Server.Implementations.Sorting
     public class ArtistComparer : IBaseItemComparer
     {
         /// <inheritdoc />
-        public string Name => ItemSortBy.Artist;
+        public ItemSortBy Type => ItemSortBy.Artist;
 
         /// <inheritdoc />
         public int Compare(BaseItem? x, BaseItem? y)

+ 2 - 1
Emby.Server.Implementations/Sorting/CommunityRatingComparer.cs

@@ -1,6 +1,7 @@
 #pragma warning disable CS1591
 
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.CommunityRating;
+        public ItemSortBy Type => ItemSortBy.CommunityRating;
 
         /// <summary>
         /// Compares the specified x.

+ 2 - 1
Emby.Server.Implementations/Sorting/CriticRatingComparer.cs

@@ -1,3 +1,4 @@
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Querying;
@@ -13,7 +14,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.CriticRating;
+        public ItemSortBy Type => ItemSortBy.CriticRating;
 
         /// <summary>
         /// Compares the specified x.

+ 2 - 1
Emby.Server.Implementations/Sorting/DateCreatedComparer.cs

@@ -1,4 +1,5 @@
 using System;
+using Jellyfin.Data.Enums;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Sorting;
 using MediaBrowser.Model.Querying;
@@ -14,7 +15,7 @@ namespace Emby.Server.Implementations.Sorting
         /// Gets the name.
         /// </summary>
         /// <value>The name.</value>
-        public string Name => ItemSortBy.DateCreated;
+        public ItemSortBy Type => ItemSortBy.DateCreated;
 
         /// <summary>
         /// Compares the specified x.

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