Przeglądaj źródła

Merge branch 'master' into mbaff-interlace-detection

Orry Verducci 3 lat temu
rodzic
commit
e446e9fde9
100 zmienionych plików z 1495 dodań i 1326 usunięć
  1. 0 1
      .ci/azure-pipelines-abi.yml
  2. 0 1
      .ci/azure-pipelines-main.yml
  3. 1 2
      .ci/azure-pipelines-package.yml
  4. 0 1
      .ci/azure-pipelines-test.yml
  5. 0 51
      .github/ISSUE_TEMPLATE/bug_report.md
  6. 106 0
      .github/ISSUE_TEMPLATE/issue report.yml
  7. 1 2
      .github/workflows/codeql-analysis.yml
  8. 124 0
      .github/workflows/openapi.yml
  9. 4 1
      Directory.Build.props
  10. 243 561
      Emby.Dlna/ContentDirectory/ControlHandler.cs
  11. 11 8
      Emby.Dlna/ContentDirectory/ServerItem.cs
  12. 1 1
      Emby.Dlna/Didl/DidlBuilder.cs
  13. 4 21
      Emby.Dlna/DlnaManager.cs
  14. 1 1
      Emby.Dlna/Emby.Dlna.csproj
  15. 5 19
      Emby.Dlna/Main/DlnaEntryPoint.cs
  16. 1 4
      Emby.Dlna/PlayTo/PlayToManager.cs
  17. 1 1
      Emby.Dlna/Service/BaseControlHandler.cs
  18. 1 1
      Emby.Drawing/ImageProcessor.cs
  19. 18 0
      Emby.Naming/Common/NamingOptions.cs
  20. 1 1
      Emby.Naming/Emby.Naming.csproj
  21. 29 0
      Emby.Naming/TV/SeriesInfo.cs
  22. 61 0
      Emby.Naming/TV/SeriesPathParser.cs
  23. 19 0
      Emby.Naming/TV/SeriesPathParserResult.cs
  24. 49 0
      Emby.Naming/TV/SeriesResolver.cs
  25. 131 184
      Emby.Server.Implementations/ApplicationHost.cs
  26. 0 8
      Emby.Server.Implementations/Channels/ChannelManager.cs
  27. 55 35
      Emby.Server.Implementations/Cryptography/CryptographyProvider.cs
  28. 1 1
      Emby.Server.Implementations/Data/ManagedConnection.cs
  29. 9 15
      Emby.Server.Implementations/Dto/DtoService.cs
  30. 7 7
      Emby.Server.Implementations/Emby.Server.Implementations.csproj
  31. 1 5
      Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs
  32. 1 1
      Emby.Server.Implementations/HttpServer/Security/AuthService.cs
  33. 6 1
      Emby.Server.Implementations/HttpServer/WebSocketManager.cs
  34. 11 13
      Emby.Server.Implementations/IO/FileRefresher.cs
  35. 6 4
      Emby.Server.Implementations/IO/LibraryMonitor.cs
  36. 35 8
      Emby.Server.Implementations/IO/ManagedFileSystem.cs
  37. 7 6
      Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs
  38. 21 43
      Emby.Server.Implementations/Library/LibraryManager.cs
  39. 13 24
      Emby.Server.Implementations/Library/MediaSourceManager.cs
  40. 26 41
      Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
  41. 12 18
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs
  42. 6 13
      Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs
  43. 7 6
      Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
  44. 3 3
      Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
  45. 10 13
      Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
  46. 6 5
      Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs
  47. 11 12
      Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
  48. 4 3
      Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs
  49. 14 16
      Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs
  50. 22 16
      Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
  51. 156 0
      Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs
  52. 1 1
      Emby.Server.Implementations/Library/Validators/PeopleValidator.cs
  53. 6 6
      Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs
  54. 9 10
      Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs
  55. 8 11
      Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs
  56. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs
  57. 3 3
      Emby.Server.Implementations/LiveTv/LiveTvManager.cs
  58. 1 1
      Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs
  59. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
  60. 2 2
      Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
  61. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
  62. 1 1
      Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
  63. 1 0
      Emby.Server.Implementations/Localization/Core/as.json
  64. 4 0
      Emby.Server.Implementations/Localization/Core/be.json
  65. 2 2
      Emby.Server.Implementations/Localization/Core/ca.json
  66. 1 1
      Emby.Server.Implementations/Localization/Core/cs.json
  67. 1 1
      Emby.Server.Implementations/Localization/Core/en-GB.json
  68. 2 2
      Emby.Server.Implementations/Localization/Core/en-US.json
  69. 9 9
      Emby.Server.Implementations/Localization/Core/eo.json
  70. 7 7
      Emby.Server.Implementations/Localization/Core/es_419.json
  71. 15 10
      Emby.Server.Implementations/Localization/Core/et.json
  72. 2 2
      Emby.Server.Implementations/Localization/Core/fa.json
  73. 1 1
      Emby.Server.Implementations/Localization/Core/fr.json
  74. 2 2
      Emby.Server.Implementations/Localization/Core/hu.json
  75. 7 5
      Emby.Server.Implementations/Localization/Core/id.json
  76. 1 1
      Emby.Server.Implementations/Localization/Core/it.json
  77. 1 1
      Emby.Server.Implementations/Localization/Core/kk.json
  78. 1 1
      Emby.Server.Implementations/Localization/Core/mk.json
  79. 14 0
      Emby.Server.Implementations/Localization/Core/mn.json
  80. 1 1
      Emby.Server.Implementations/Localization/Core/ms.json
  81. 3 3
      Emby.Server.Implementations/Localization/Core/pl.json
  82. 3 1
      Emby.Server.Implementations/Localization/Core/pr.json
  83. 5 3
      Emby.Server.Implementations/Localization/Core/pt-PT.json
  84. 2 2
      Emby.Server.Implementations/Localization/Core/ru.json
  85. 1 1
      Emby.Server.Implementations/Localization/Core/sk.json
  86. 2 2
      Emby.Server.Implementations/Localization/Core/sq.json
  87. 1 1
      Emby.Server.Implementations/Localization/Core/sr.json
  88. 1 1
      Emby.Server.Implementations/Localization/Core/sv.json
  89. 1 1
      Emby.Server.Implementations/Localization/Core/ta.json
  90. 23 0
      Emby.Server.Implementations/Localization/Core/te.json
  91. 1 1
      Emby.Server.Implementations/Localization/Core/tr.json
  92. 1 1
      Emby.Server.Implementations/Localization/Core/uk.json
  93. 6 6
      Emby.Server.Implementations/Localization/Core/vi.json
  94. 1 1
      Emby.Server.Implementations/Localization/Core/zh-CN.json
  95. 1 0
      Emby.Server.Implementations/Localization/Core/zu.json
  96. 68 35
      Emby.Server.Implementations/Localization/LocalizationManager.cs
  97. 1 1
      Emby.Server.Implementations/Localization/countries.json
  98. 2 1
      Emby.Server.Implementations/Localization/iso6392.txt
  99. 1 1
      Emby.Server.Implementations/Playlists/PlaylistManager.cs
  100. 3 2
      Emby.Server.Implementations/Plugins/PluginManager.cs

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

@@ -34,7 +34,6 @@ jobs:
         inputs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
-          includePreviewVersions: true
 
       - task: DotNetCoreCLI@2
         displayName: 'Install ABI CompatibilityChecker Tool'

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

@@ -54,7 +54,6 @@ jobs:
         inputs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
-          includePreviewVersions: true
 
       - task: DotNetCoreCLI@2
         displayName: 'Publish Server'

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

@@ -181,7 +181,7 @@ jobs:
     inputs:
       sshEndpoint: repository
       runOptions: 'commands'
-      commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) &
+      commands: nohup sudo /srv/repository/collect-server.azure.sh /srv/repository/incoming/azure $(Build.BuildNumber) $(Build.SourceBranch) &
 
 - job: PublishNuget
   displayName: 'Publish NuGet packages'
@@ -199,7 +199,6 @@ jobs:
     inputs:
       packageType: 'sdk'
       version: '6.0.x'
-      includePreviewVersions: true
 
   - task: DotNetCoreCLI@2
     displayName: 'Build Stable Nuget packages'

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

@@ -41,7 +41,6 @@ jobs:
         inputs:
           packageType: sdk
           version: ${{ parameters.DotNetSdkVersion }}
-          includePreviewVersions: true
 
       - task: SonarCloudPrepare@1
         displayName: 'Prepare analysis on SonarCloud'

+ 0 - 51
.github/ISSUE_TEMPLATE/bug_report.md

@@ -1,51 +0,0 @@
----
-name: Bug report
-about: Create a bug report
-title: ''
-labels: bug
-assignees: ''
-
----
-
-**Describe the bug**
-<!-- A clear and concise description of what the bug is. -->
-
-**System (please complete the following information):**
- - OS: [e.g. Debian, Windows]
- - Virtualization: [e.g. Docker, KVM, LXC]
- - Clients: [Browser, Android, Fire Stick, etc.]
- - Browser: [e.g. Firefox 91, Chrome 93, Safari 13]
- - Jellyfin Version: [e.g. 10.7.6, unstable 20191231]
- - FFmpeg Version: [e.g. 4.3.2-Jellyfin]
- - Playback: [Direct Play, Remux, Direct Stream, Transcode] 
- - Hardware Acceleration: [e.g. none, VAAPI, NVENC, etc.]
- - Installed Plugins: [e.g. none, Fanart, Anime, etc.]
- - Reverse Proxy: [e.g. none, nginx, apache, etc.]
- - Base URL: [e.g. none, yes: /example]
- - Networking: [e.g. Host, Bridge/NAT]
- - Storage: [e.g. local, NFS, cloud]
-
-**To Reproduce**
-<!-- Steps to reproduce the behavior: -->
-1. Go to '...'
-2. Click on '....'
-3. Scroll down to '....'
-4. See error
-
-**Expected behavior**
-<!-- A clear and concise description of what you expected to happen. -->
-
-**Server Logs**
-<!-- Please paste any log errors. -->
-
-**FFmpeg Logs**
-<!-- Please paste any log errors. -->
-
-**Browser Console Logs**
-<!-- Please paste any log errors. -->
-
-**Screenshots**
-<!-- If applicable, add screenshots to help explain your problem. -->
-
-**Additional context**
-<!-- Add any other context about the problem here. -->

+ 106 - 0
.github/ISSUE_TEMPLATE/issue report.yml

@@ -0,0 +1,106 @@
+name: Issue Report
+description: File an issue report
+title: "[Issue]: "
+labels: [bug, triage]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to fill out this bug report! Please provide as much detail as necessary, most questions may not be applicable to you. If you need real-time help, join us on [Matrix](https://matrix.to/#/#jellyfin-troubleshooting:matrix.org) or [Discord](https://discord.gg/zHBxVSXdBV).
+  - type: textarea
+    id: what-happened
+    attributes:
+      label: Please describe your bug
+      description: Also tell us, what did you expect to happen?
+      placeholder: |
+        The more information that you are able to provide, the better. Did you do anything before this happened? Did you upgrade or change anything? Any screenshots or logs you can provide will be helpful.
+
+        This is my issue.
+
+        Steps to Reproduce
+          1. In this environment...
+          2. With this config...
+          3. Run '...'
+          4. See error...
+    validations:
+      required: true
+  - type: dropdown
+    id: version
+    attributes:
+      label: Jellyfin Version
+      description: What version of Jellyfin are you running?
+      options:
+        - 10.7.7
+        - 10.7.z
+        - 10.6.4
+        - Other
+    validations:
+      required: true
+  - type: input
+    id: version-other
+    attributes:
+      label: "if other:"
+      placeholder: Other
+  - type: textarea
+    attributes:
+      label: Environment
+      description: |
+        Examples:
+        - **OS**: [e.g. Debian, Windows]
+        - **Virtualization**: [e.g. Docker, KVM, LXC]
+        - **Clients**: [Browser, Android, Fire Stick, etc.]
+        - **Browser**: [e.g. Firefox 91, Chrome 93, Safari 13]
+        - **FFmpeg Version**: [e.g. 4.3.2-Jellyfin]
+        - **Playback**: [Direct Play, Remux, Direct Stream, Transcode]
+        - **Hardware Acceleration**: [e.g. none, VAAPI, NVENC, etc.]
+        - **Installed Plugins**: [e.g. none, Fanart, Anime, etc.]
+        - **Reverse Proxy**: [e.g. none, nginx, apache, etc.]
+        - **Base URL**: [e.g. none, yes: /example]
+        - **Networking**: [e.g. Host, Bridge/NAT]
+        - **Storage**: [e.g. local, NFS, cloud]
+      value: |
+        - OS:
+        - Virtualization:
+        - Clients:
+        - Browser:
+        - FFmpeg Version:
+        - Playback Method:
+        - Hardware Acceleration:
+        - Plugins:
+        - Reverse Proxy:
+        - Base URL:
+        - Networking:
+        - Storage:
+      render: markdown
+  - type: textarea
+    id: logs
+    attributes:
+      label: Jellyfin logs
+      description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+      placeholder: For playback issues, browser/client and FFmpeg logs may be more useful.
+      render: shell
+  - type: textarea
+    id: ffmpeg-logs
+    attributes:
+      label: FFmpeg logs
+      description: Please copy and paste any relevant log output. This can be found in Dashboard > Logs.
+      placeholder: It's important to include the specific codec details. If no FFmpeg logs appear, the file was Direct Played and did not use FFmpeg.
+      render: shell
+  - type: textarea
+    id: browserlogs
+    attributes:
+      label: Please attach any browser or client logs here
+      placeholder: Access browser logs by using the F12 to bring up the console. Screenshots are typically easier to read than raw logs. For clients such as Android or iOS, please see our documentation.
+  - type: textarea
+    id: screenshots
+    attributes:
+      label: Please attach any screenshots here
+      placeholder: Images can be pasted directly into the textbox and will be hosted by github.
+  - type: checkboxes
+    id: terms
+    attributes:
+      label: Code of Conduct
+      description: By submitting this issue, you agree to follow our [Code of Conduct](https://jellyfin.org/docs/general/community-standards.html#code-of-conduct)
+      options:
+        - label: I agree to follow this project's Code of Conduct
+          required: true

+ 1 - 2
.github/workflows/codeql-analysis.yml

@@ -25,8 +25,7 @@ jobs:
       uses: actions/setup-dotnet@v1
       with:
         dotnet-version: '6.0.x'
-        include-prerelease: true
-        
+
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v1
       with:

+ 124 - 0
.github/workflows/openapi.yml

@@ -0,0 +1,124 @@
+name: OpenAPI
+on:
+  push:
+    branches:
+      - master
+  pull_request_target:
+
+jobs:
+  openapi-head:
+    name: OpenAPI - HEAD
+    runs-on: ubuntu-latest
+    permissions: read-all
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.event.pull_request.head.ref }}
+          repository: ${{ github.event.pull_request.head.repo.full_name }}
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v1
+        with:
+          dotnet-version: '6.0.x'
+      - name: Generate openapi.json
+        run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
+      - name: Upload openapi.json
+        uses: actions/upload-artifact@v2
+        with:
+          name: openapi-head
+          retention-days: 14
+          if-no-files-found: error
+          path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+  openapi-base:
+    name: OpenAPI - BASE
+    if: ${{ github.base_ref != '' }}
+    runs-on: ubuntu-latest
+    permissions: read-all
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.base_ref }}
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v1
+        with:
+          dotnet-version: '6.0.x'
+      - name: Generate openapi.json
+        run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
+      - name: Upload openapi.json
+        uses: actions/upload-artifact@v2
+        with:
+          name: openapi-base
+          retention-days: 14
+          if-no-files-found: error
+          path: tests/Jellyfin.Server.Integration.Tests/bin/Release/net6.0/openapi.json
+
+  openapi-diff:
+    name: OpenAPI - Difference
+    if: ${{ github.event_name == 'pull_request_target' }}
+    runs-on: ubuntu-latest
+    needs:
+      - openapi-head
+      - openapi-base
+    steps:
+      - name: Download openapi-head
+        uses: actions/download-artifact@v2
+        with:
+          name: openapi-head
+          path: openapi-head
+      - name: Download openapi-base
+        uses: actions/download-artifact@v2
+        with:
+          name: openapi-base
+          path: openapi-base
+      - name: Workaround openapi-diff issue
+        run: |
+          sed -i 's/"allOf"/"oneOf"/g' openapi-head/openapi.json
+          sed -i 's/"allOf"/"oneOf"/g' openapi-base/openapi.json
+      - name: Calculate OpenAPI difference
+        uses: docker://openapitools/openapi-diff
+        continue-on-error: true
+        with:
+          args: --fail-on-changed --markdown openapi-changes.md openapi-base/openapi.json openapi-head/openapi.json
+      - id: read-diff
+        name: Read openapi-diff output
+        run: |
+          body=$(cat openapi-changes.md)
+          body="${body//'%'/'%25'}"
+          body="${body//$'\n'/'%0A'}"
+          body="${body//$'\r'/'%0D'}"
+          echo ::set-output name=body::$body
+      - name: Find difference comment
+        uses: peter-evans/find-comment@v1
+        id: find-comment
+        with:
+          issue-number: ${{ github.event.pull_request.number }}
+          direction: last
+          body-includes: openapi-diff-workflow-comment
+      - name: Reply or edit difference comment (changed)
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ steps.read-diff.outputs.body != '' }}
+        with:
+          issue-number: ${{ github.event.pull_request.number }}
+          comment-id: ${{ steps.find-comment.outputs.comment-id }}
+          edit-mode: replace
+          body: |
+            <!--openapi-diff-workflow-comment-->
+            <details>
+            <summary>Changes in OpenAPI specification found. Expand to see details.</summary>
+
+            ${{ steps.read-diff.outputs.body }}
+
+            </details>
+      - name: Edit difference comment (unchanged)
+        uses: peter-evans/create-or-update-comment@v1.4.5
+        if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
+        with:
+          issue-number: ${{ github.event.pull_request.number }}
+          comment-id: ${{ steps.find-comment.outputs.comment-id }}
+          edit-mode: replace
+          body: |
+            <!--openapi-diff-workflow-comment-->
+
+            No changes to OpenAPI specification found. See history of this comment for previous changes.

+ 4 - 1
Directory.Build.props

@@ -3,10 +3,13 @@
 
   <PropertyGroup>
     <Nullable>enable</Nullable>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
 
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+  </PropertyGroup>
+
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
     <AnalysisMode>AllEnabledByDefault</AnalysisMode>
   </PropertyGroup>

Plik diff jest za duży
+ 243 - 561
Emby.Dlna/ContentDirectory/ControlHandler.cs


+ 11 - 8
Emby.Dlna/ContentDirectory/ServerItem.cs

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using MediaBrowser.Controller.Entities;
 
 namespace Emby.Dlna.ContentDirectory
@@ -13,24 +11,29 @@ namespace Emby.Dlna.ContentDirectory
         /// Initializes a new instance of the <see cref="ServerItem"/> class.
         /// </summary>
         /// <param name="item">The <see cref="BaseItem"/>.</param>
-        public ServerItem(BaseItem item)
+        /// <param name="stubType">The stub type.</param>
+        public ServerItem(BaseItem item, StubType? stubType)
         {
             Item = item;
 
-            if (item is IItemByName && item is not Folder)
+            if (stubType.HasValue)
+            {
+                StubType = stubType;
+            }
+            else if (item is IItemByName and not Folder)
             {
                 StubType = Dlna.ContentDirectory.StubType.Folder;
             }
         }
 
         /// <summary>
-        /// Gets or sets the underlying base item.
+        /// Gets the underlying base item.
         /// </summary>
-        public BaseItem Item { get; set; }
+        public BaseItem Item { get; }
 
         /// <summary>
-        /// Gets or sets the DLNA item type.
+        /// Gets the DLNA item type.
         /// </summary>
-        public StubType? StubType { get; set; }
+        public StubType? StubType { get; }
     }
 }

+ 1 - 1
Emby.Dlna/Didl/DidlBuilder.cs

@@ -729,7 +729,7 @@ namespace Emby.Dlna.Didl
             {
                 if (item.PremiereDate.HasValue)
                 {
-                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc);
+                    AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc);
                 }
             }
 

+ 4 - 21
Emby.Dlna/DlnaManager.cs

@@ -112,7 +112,7 @@ namespace Emby.Dlna
 
             if (profile == null)
             {
-                LogUnmatchedProfile(deviceInfo);
+                _logger.LogInformation("No matching device profile found. The default will need to be used. \n{@Profile}", deviceInfo);
             }
             else
             {
@@ -122,23 +122,6 @@ namespace Emby.Dlna
             return profile;
         }
 
-        private void LogUnmatchedProfile(DeviceIdentification profile)
-        {
-            var builder = new StringBuilder();
-
-            builder.AppendLine("No matching device profile found. The default will need to be used.");
-            builder.Append("FriendlyName: ").AppendLine(profile.FriendlyName);
-            builder.Append("Manufacturer: ").AppendLine(profile.Manufacturer);
-            builder.Append("ManufacturerUrl: ").AppendLine(profile.ManufacturerUrl);
-            builder.Append("ModelDescription: ").AppendLine(profile.ModelDescription);
-            builder.Append("ModelName: ").AppendLine(profile.ModelName);
-            builder.Append("ModelNumber: ").AppendLine(profile.ModelNumber);
-            builder.Append("ModelUrl: ").AppendLine(profile.ModelUrl);
-            builder.Append("SerialNumber: ").AppendLine(profile.SerialNumber);
-
-            _logger.LogInformation(builder.ToString());
-        }
-
         /// <summary>
         /// Attempts to match a device with a profile.
         /// Rules:
@@ -367,7 +350,7 @@ namespace Emby.Dlna
                         Directory.CreateDirectory(systemProfilesPath);
 
                         var fileOptions = AsyncFile.WriteOptions;
-                        fileOptions.Mode = FileMode.CreateNew;
+                        fileOptions.Mode = FileMode.Create;
                         fileOptions.PreallocationSize = length;
                         using (var fileStream = new FileStream(path, fileOptions))
                         {
@@ -416,7 +399,7 @@ namespace Emby.Dlna
         }
 
         /// <inheritdoc />
-        public void UpdateProfile(DeviceProfile profile)
+        public void UpdateProfile(string profileId, DeviceProfile profile)
         {
             profile = ReserializeProfile(profile);
 
@@ -430,7 +413,7 @@ namespace Emby.Dlna
                 throw new ArgumentException("Profile is missing Name");
             }
 
-            var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
+            var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profileId, StringComparison.OrdinalIgnoreCase));
 
             var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".xml";
             var path = Path.Combine(UserProfilesPath, newFilename);

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

@@ -72,7 +72,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" />
+    <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
   </ItemGroup>
 
 </Project>

+ 5 - 19
Emby.Dlna/Main/DlnaEntryPoint.cs

@@ -52,7 +52,6 @@ namespace Emby.Dlna.Main
         private readonly ISocketFactory _socketFactory;
         private readonly INetworkManager _networkManager;
         private readonly object _syncLock = new object();
-        private readonly NetworkConfiguration _netConfig;
         private readonly bool _disabled;
 
         private PlayToManager _manager;
@@ -125,8 +124,8 @@ namespace Emby.Dlna.Main
                 config);
             Current = this;
 
-            _netConfig = config.GetConfiguration<NetworkConfiguration>("network");
-            _disabled = appHost.ListenWithHttps && _netConfig.RequireHttps;
+            var netConfig = config.GetConfiguration<NetworkConfiguration>("network");
+            _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
 
             if (_disabled && _config.GetDlnaConfiguration().EnableServer)
             {
@@ -219,11 +218,6 @@ namespace Emby.Dlna.Main
             }
         }
 
-        private void LogMessage(string msg)
-        {
-            _logger.LogDebug(msg);
-        }
-
         private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
         {
             try
@@ -268,12 +262,11 @@ namespace Emby.Dlna.Main
             {
                 _publisher = new SsdpDevicePublisher(
                     _communicationsServer,
-                    _networkManager,
                     MediaBrowser.Common.System.OperatingSystem.Name,
                     Environment.OSVersion.VersionString,
                     _config.GetDlnaConfiguration().SendOnlyMatchedHost)
                 {
-                    LogFunction = LogMessage,
+                    LogFunction = (msg) => _logger.LogDebug("{Msg}", msg),
                     SupportPnpRootDevice = false
                 };
 
@@ -318,15 +311,9 @@ namespace Emby.Dlna.Main
 
                 var fullService = "urn:schemas-upnp-org:device:MediaServer:1";
 
-                _logger.LogInformation("Registering publisher for {0} on {1}", fullService, address);
+                _logger.LogInformation("Registering publisher for {ResourceName} on {DeviceAddress}", fullService, address);
 
-                var uri = new UriBuilder(_appHost.GetSmartApiUrl(address.Address) + descriptorUri);
-                if (!string.IsNullOrEmpty(_appHost.PublishedServerUrl))
-                {
-                    // DLNA will only work over http, so we must reset to http:// : {port}.
-                    uri.Scheme = "http";
-                    uri.Port = _netConfig.HttpServerPortNumber;
-                }
+                var uri = new UriBuilder(_appHost.GetApiUrlForLocalAccess(false) + descriptorUri);
 
                 var device = new SsdpRootDevice
                 {
@@ -412,7 +399,6 @@ namespace Emby.Dlna.Main
                         _imageProcessor,
                         _deviceDiscovery,
                         _httpClientFactory,
-                        _config,
                         _userDataManager,
                         _localization,
                         _mediaSourceManager,

+ 1 - 4
Emby.Dlna/PlayTo/PlayToManager.cs

@@ -11,7 +11,6 @@ using System.Threading.Tasks;
 using Jellyfin.Data.Events;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Controller;
-using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Library;
@@ -35,7 +34,6 @@ namespace Emby.Dlna.PlayTo
         private readonly IServerApplicationHost _appHost;
         private readonly IImageProcessor _imageProcessor;
         private readonly IHttpClientFactory _httpClientFactory;
-        private readonly IServerConfigurationManager _config;
         private readonly IUserDataManager _userDataManager;
         private readonly ILocalizationManager _localization;
 
@@ -47,7 +45,7 @@ namespace Emby.Dlna.PlayTo
         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, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
+        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)
         {
             _logger = logger;
             _sessionManager = sessionManager;
@@ -58,7 +56,6 @@ namespace Emby.Dlna.PlayTo
             _imageProcessor = imageProcessor;
             _deviceDiscovery = deviceDiscovery;
             _httpClientFactory = httpClientFactory;
-            _config = config;
             _userDataManager = userDataManager;
             _localization = localization;
             _mediaSourceManager = mediaSourceManager;

+ 1 - 1
Emby.Dlna/Service/BaseControlHandler.cs

@@ -64,7 +64,7 @@ namespace Emby.Dlna.Service
                 requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false);
             }
 
-            Logger.LogDebug("Received control request {0}", requestInfo.LocalName);
+            Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
 
             var settings = new XmlWriterSettings
             {

+ 1 - 1
Emby.Drawing/ImageProcessor.cs

@@ -26,7 +26,7 @@ namespace Emby.Drawing
     public sealed class ImageProcessor : IImageProcessor, IDisposable
     {
         // Increment this when there's a change requiring caches to be invalidated
-        private const string Version = "3";
+        private const char Version = '3';
 
         private static readonly HashSet<string> _transparentImageTypes
             = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" };

+ 18 - 0
Emby.Naming/Common/NamingOptions.cs

@@ -1,3 +1,5 @@
+#pragma warning disable CA1819
+
 using System;
 using System.Linq;
 using System.Text.RegularExpressions;
@@ -253,6 +255,8 @@ namespace Emby.Naming.Common
                 },
                 // <!-- foo.ep01, foo.EP_01 -->
                 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)
                 {
                     DateTimeFormats = new[]
@@ -371,6 +375,20 @@ namespace Emby.Naming.Common
                     IsOptimistic = true,
                     IsNamed = true
                 },
+
+                // Series and season only expression
+                // "the show/season 1", "the show/s01"
+                new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)\/[Ss](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
+                {
+                    IsNamed = true
+                },
+
+                // Series and season only expression
+                // "the show S01", "the show season 1"
+                new EpisodeExpression(@"(.*(\\|\/))*(?<seriesname>.+)[\. _\-]+[sS](eason)?[\. _\-]*(?<seasonnumber>[0-9]+)")
+                {
+                    IsNamed = true
+                },
             };
 
             EpisodeWithoutSeasonExpressions = new[]

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

@@ -38,7 +38,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
+    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
   </ItemGroup>
 
   <!-- Code Analyzers-->

+ 29 - 0
Emby.Naming/TV/SeriesInfo.cs

@@ -0,0 +1,29 @@
+namespace Emby.Naming.TV
+{
+    /// <summary>
+    /// Holder object for Series information.
+    /// </summary>
+    public class SeriesInfo
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="SeriesInfo"/> class.
+        /// </summary>
+        /// <param name="path">Path to the file.</param>
+        public SeriesInfo(string path)
+        {
+            Path = path;
+        }
+
+        /// <summary>
+        /// Gets or sets the path.
+        /// </summary>
+        /// <value>The path.</value>
+        public string Path { get; set; }
+
+        /// <summary>
+        /// Gets or sets the name of the series.
+        /// </summary>
+        /// <value>The name of the series.</value>
+        public string? Name { get; set; }
+    }
+}

+ 61 - 0
Emby.Naming/TV/SeriesPathParser.cs

@@ -0,0 +1,61 @@
+using System.Globalization;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+    /// <summary>
+    /// Used to parse information about series from paths containing more information that only the series name.
+    /// Uses the same regular expressions as the EpisodePathParser but have different success criteria.
+    /// </summary>
+    public static class SeriesPathParser
+    {
+        /// <summary>
+        /// Parses information about series from path.
+        /// </summary>
+        /// <param name="options"><see cref="NamingOptions"/> object containing EpisodeExpressions and MultipleEpisodeExpressions.</param>
+        /// <param name="path">Path.</param>
+        /// <returns>Returns <see cref="SeriesPathParserResult"/> object.</returns>
+        public static SeriesPathParserResult Parse(NamingOptions options, string path)
+        {
+            SeriesPathParserResult? result = null;
+
+            foreach (var expression in options.EpisodeExpressions)
+            {
+                var currentResult = Parse(path, expression);
+                if (currentResult.Success)
+                {
+                    result = currentResult;
+                    break;
+                }
+            }
+
+            if (result != null)
+            {
+                if (!string.IsNullOrEmpty(result.SeriesName))
+                {
+                    result.SeriesName = result.SeriesName.Trim(' ', '_', '.', '-');
+                }
+            }
+
+            return result ?? new SeriesPathParserResult();
+        }
+
+        private static SeriesPathParserResult Parse(string name, EpisodeExpression expression)
+        {
+            var result = new SeriesPathParserResult();
+
+            var match = expression.Regex.Match(name);
+
+            if (match.Success && match.Groups.Count >= 3)
+            {
+                if (expression.IsNamed)
+                {
+                    result.SeriesName = match.Groups["seriesname"].Value;
+                    result.Success = !string.IsNullOrEmpty(result.SeriesName) && !string.IsNullOrEmpty(match.Groups["seasonnumber"]?.Value);
+                }
+            }
+
+            return result;
+        }
+    }
+}

+ 19 - 0
Emby.Naming/TV/SeriesPathParserResult.cs

@@ -0,0 +1,19 @@
+namespace Emby.Naming.TV
+{
+    /// <summary>
+    /// Holder object for <see cref="SeriesPathParser"/> result.
+    /// </summary>
+    public class SeriesPathParserResult
+    {
+        /// <summary>
+        /// Gets or sets the name of the series.
+        /// </summary>
+        /// <value>The name of the series.</value>
+        public string? SeriesName { get; set; }
+
+        /// <summary>
+        /// Gets or sets a value indicating whether parsing was successful.
+        /// </summary>
+        public bool Success { get; set; }
+    }
+}

+ 49 - 0
Emby.Naming/TV/SeriesResolver.cs

@@ -0,0 +1,49 @@
+using System.IO;
+using System.Text.RegularExpressions;
+using Emby.Naming.Common;
+
+namespace Emby.Naming.TV
+{
+    /// <summary>
+    /// Used to resolve information about series from path.
+    /// </summary>
+    public static class SeriesResolver
+    {
+        /// <summary>
+        /// Regex that matches strings of at least 2 characters separated by a dot or underscore.
+        /// Used for removing separators between words, i.e turns "The_show" into "The show" while
+        /// preserving namings like "S.H.O.W".
+        /// </summary>
+        private static readonly Regex _seriesNameRegex = new Regex(@"((?<a>[^\._]{2,})[\._]*)|([\._](?<b>[^\._]{2,}))");
+
+        /// <summary>
+        /// Resolve information about series from path.
+        /// </summary>
+        /// <param name="options"><see cref="NamingOptions"/> object passed to <see cref="SeriesPathParser"/>.</param>
+        /// <param name="path">Path to series.</param>
+        /// <returns>SeriesInfo.</returns>
+        public static SeriesInfo Resolve(NamingOptions options, string path)
+        {
+            string seriesName = Path.GetFileName(path);
+
+            SeriesPathParserResult result = SeriesPathParser.Parse(options, path);
+            if (result.Success)
+            {
+                if (!string.IsNullOrEmpty(result.SeriesName))
+                {
+                    seriesName = result.SeriesName;
+                }
+            }
+
+            if (!string.IsNullOrEmpty(seriesName))
+            {
+                seriesName = _seriesNameRegex.Replace(seriesName, "${a} ${b}").Trim();
+            }
+
+            return new SeriesInfo(path)
+            {
+                Name = seriesName
+            };
+        }
+    }
+}

+ 131 - 184
Emby.Server.Implementations/ApplicationHost.cs

@@ -3,6 +3,7 @@
 #pragma warning disable CS1591
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Globalization;
@@ -18,6 +19,7 @@ using Emby.Dlna;
 using Emby.Dlna.Main;
 using Emby.Dlna.Ssdp;
 using Emby.Drawing;
+using Emby.Naming.Common;
 using Emby.Notifications;
 using Emby.Photos;
 using Emby.Server.Implementations.Archiving;
@@ -56,6 +58,7 @@ using MediaBrowser.Common.Updates;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Channels;
 using MediaBrowser.Controller.Chapters;
+using MediaBrowser.Controller.ClientEvent;
 using MediaBrowser.Controller.Collections;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Dlna;
@@ -117,7 +120,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// The disposable parts.
         /// </summary>
-        private readonly List<IDisposable> _disposableParts = new List<IDisposable>();
+        private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new ();
 
         private readonly IFileSystem _fileSystemManager;
         private readonly IConfiguration _startupConfig;
@@ -128,7 +131,6 @@ namespace Emby.Server.Implementations
         private List<Type> _creatingInstances;
         private IMediaEncoder _mediaEncoder;
         private ISessionManager _sessionManager;
-        private string[] _urlPrefixes;
 
         /// <summary>
         /// Gets or sets all concrete types.
@@ -147,25 +149,20 @@ namespace Emby.Server.Implementations
         /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param>
         /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param>
         /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param>
-        /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
-        /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
         public ApplicationHost(
             IServerApplicationPaths applicationPaths,
             ILoggerFactory loggerFactory,
             IStartupOptions options,
-            IConfiguration startupConfig,
-            IFileSystem fileSystem,
-            IServiceCollection serviceCollection)
+            IConfiguration startupConfig)
         {
             ApplicationPaths = applicationPaths;
             LoggerFactory = loggerFactory;
             _startupOptions = options;
             _startupConfig = startupConfig;
-            _fileSystemManager = fileSystem;
-            ServiceCollection = serviceCollection;
+            _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
 
             Logger = LoggerFactory.CreateLogger<ApplicationHost>();
-            fileSystem.AddShortcutHandler(new MbLinkShortcutHandler(fileSystem));
+            _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
 
             ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
             ApplicationVersionString = ApplicationVersion.ToString(3);
@@ -214,7 +211,7 @@ namespace Emby.Server.Implementations
         /// <summary>
         /// Gets the <see cref="INetworkManager"/> singleton instance.
         /// </summary>
-        public INetworkManager NetManager { get; internal set; }
+        public INetworkManager NetManager { get; private set; }
 
         /// <summary>
         /// Gets a value indicating whether this instance has changes that require the entire application to restart.
@@ -230,24 +227,22 @@ namespace Emby.Server.Implementations
         /// </summary>
         protected ILogger<ApplicationHost> Logger { get; }
 
-        protected IServiceCollection ServiceCollection { get; }
-
         /// <summary>
         /// Gets the logger factory.
         /// </summary>
         protected ILoggerFactory LoggerFactory { get; }
 
         /// <summary>
-        /// Gets or sets the application paths.
+        /// Gets the application paths.
         /// </summary>
         /// <value>The application paths.</value>
-        protected IServerApplicationPaths ApplicationPaths { get; set; }
+        protected IServerApplicationPaths ApplicationPaths { get; }
 
         /// <summary>
-        /// Gets or sets the configuration manager.
+        /// Gets the configuration manager.
         /// </summary>
         /// <value>The configuration manager.</value>
-        public ServerConfigurationManager ConfigurationManager { get; set; }
+        public ServerConfigurationManager ConfigurationManager { get; }
 
         /// <summary>
         /// Gets or sets the service provider.
@@ -350,22 +345,6 @@ namespace Emby.Server.Implementations
                 .Replace(appPaths.InternalMetadataPath, appPaths.VirtualInternalMetadataPath, StringComparison.OrdinalIgnoreCase);
         }
 
-        /// <summary>
-        /// Creates an instance of type and resolves all constructor dependencies.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>System.Object.</returns>
-        public object CreateInstance(Type type)
-            => ActivatorUtilities.CreateInstance(ServiceProvider, type);
-
-        /// <summary>
-        /// Creates an instance of type and resolves all constructor dependencies.
-        /// </summary>
-        /// <typeparam name="T">The type.</typeparam>
-        /// <returns>T.</returns>
-        public T CreateInstance<T>()
-            => ActivatorUtilities.CreateInstance<T>(ServiceProvider);
-
         /// <summary>
         /// Creates the instance safe.
         /// </summary>
@@ -375,7 +354,7 @@ namespace Emby.Server.Implementations
         {
             _creatingInstances ??= new List<Type>();
 
-            if (_creatingInstances.IndexOf(type) != -1)
+            if (_creatingInstances.Contains(type))
             {
                 Logger.LogError("DI Loop detected in the attempted creation of {Type}", type.FullName);
                 foreach (var entry in _creatingInstances)
@@ -385,7 +364,7 @@ namespace Emby.Server.Implementations
 
                 _pluginManager.FailPlugin(type.Assembly);
 
-                throw new ExternalException("DI Loop detected.");
+                throw new TypeLoadException("DI Loop detected");
             }
 
             try
@@ -418,8 +397,15 @@ namespace Emby.Server.Implementations
         public IEnumerable<Type> GetExportTypes<T>()
         {
             var currentType = typeof(T);
-
-            return _allConcreteTypes.Where(i => currentType.IsAssignableFrom(i));
+            var numberOfConcreteTypes = _allConcreteTypes.Length;
+            for (var i = 0; i < numberOfConcreteTypes; i++)
+            {
+                var type = _allConcreteTypes[i];
+                if (currentType.IsAssignableFrom(type))
+                {
+                    yield return type;
+                }
+            }
         }
 
         /// <inheritdoc />
@@ -434,9 +420,9 @@ namespace Emby.Server.Implementations
 
             if (manageLifetime)
             {
-                lock (_disposableParts)
+                foreach (var part in parts.OfType<IDisposable>())
                 {
-                    _disposableParts.AddRange(parts.OfType<IDisposable>());
+                    _disposableParts.TryAdd(part, byte.MinValue);
                 }
             }
 
@@ -455,9 +441,9 @@ namespace Emby.Server.Implementations
 
             if (manageLifetime)
             {
-                lock (_disposableParts)
+                foreach (var part in parts.OfType<IDisposable>())
                 {
-                    _disposableParts.AddRange(parts.OfType<IDisposable>());
+                    _disposableParts.TryAdd(part, byte.MinValue);
                 }
             }
 
@@ -521,7 +507,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public void Init()
+        public void Init(IServiceCollection serviceCollection)
         {
             DiscoverTypes();
 
@@ -551,128 +537,130 @@ namespace Emby.Server.Implementations
             CertificatePath = networkConfiguration.CertificatePath;
             Certificate = GetCertificate(CertificatePath, networkConfiguration.CertificatePassword);
 
-            RegisterServices();
+            RegisterServices(serviceCollection);
 
-            _pluginManager.RegisterServices(ServiceCollection);
+            _pluginManager.RegisterServices(serviceCollection);
         }
 
         /// <summary>
         /// Registers services/resources with the service collection that will be available via DI.
         /// </summary>
-        protected virtual void RegisterServices()
+        /// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
+        protected virtual void RegisterServices(IServiceCollection serviceCollection)
         {
-            ServiceCollection.AddSingleton(_startupOptions);
+            serviceCollection.AddSingleton(_startupOptions);
 
-            ServiceCollection.AddMemoryCache();
+            serviceCollection.AddMemoryCache();
 
-            ServiceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
-            ServiceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
-            ServiceCollection.AddSingleton<IApplicationHost>(this);
-            ServiceCollection.AddSingleton<IPluginManager>(_pluginManager);
-            ServiceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
+            serviceCollection.AddSingleton<IServerConfigurationManager>(ConfigurationManager);
+            serviceCollection.AddSingleton<IConfigurationManager>(ConfigurationManager);
+            serviceCollection.AddSingleton<IApplicationHost>(this);
+            serviceCollection.AddSingleton(_pluginManager);
+            serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
 
-            ServiceCollection.AddSingleton(_fileSystemManager);
-            ServiceCollection.AddSingleton<TmdbClientManager>();
+            serviceCollection.AddSingleton(_fileSystemManager);
+            serviceCollection.AddSingleton<TmdbClientManager>();
 
-            ServiceCollection.AddSingleton(NetManager);
+            serviceCollection.AddSingleton(NetManager);
 
-            ServiceCollection.AddSingleton<ITaskManager, TaskManager>();
+            serviceCollection.AddSingleton<ITaskManager, TaskManager>();
 
-            ServiceCollection.AddSingleton(_xmlSerializer);
+            serviceCollection.AddSingleton(_xmlSerializer);
 
-            ServiceCollection.AddSingleton<IStreamHelper, StreamHelper>();
+            serviceCollection.AddSingleton<IStreamHelper, StreamHelper>();
 
-            ServiceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
+            serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>();
 
-            ServiceCollection.AddSingleton<ISocketFactory, SocketFactory>();
+            serviceCollection.AddSingleton<ISocketFactory, SocketFactory>();
 
-            ServiceCollection.AddSingleton<IInstallationManager, InstallationManager>();
+            serviceCollection.AddSingleton<IInstallationManager, InstallationManager>();
 
-            ServiceCollection.AddSingleton<IZipClient, ZipClient>();
+            serviceCollection.AddSingleton<IZipClient, ZipClient>();
 
-            ServiceCollection.AddSingleton<IServerApplicationHost>(this);
-            ServiceCollection.AddSingleton<IServerApplicationPaths>(ApplicationPaths);
+            serviceCollection.AddSingleton<IServerApplicationHost>(this);
+            serviceCollection.AddSingleton(ApplicationPaths);
 
-            ServiceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
+            serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
-            ServiceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
 
-            ServiceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
-            ServiceCollection.AddSingleton<IUserDataManager, UserDataManager>();
+            serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
+            serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 
-            ServiceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
+            serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
 
-            ServiceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
-            ServiceCollection.AddSingleton<EncodingHelper>();
+            serviceCollection.AddSingleton<IMediaEncoder, MediaBrowser.MediaEncoding.Encoder.MediaEncoder>();
+            serviceCollection.AddSingleton<EncodingHelper>();
 
             // TODO: Refactor to eliminate the circular dependencies here so that Lazy<T> isn't required
-            ServiceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
-            ServiceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
-            ServiceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
-            ServiceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+            serviceCollection.AddTransient(provider => new Lazy<ILibraryMonitor>(provider.GetRequiredService<ILibraryMonitor>));
+            serviceCollection.AddTransient(provider => new Lazy<IProviderManager>(provider.GetRequiredService<IProviderManager>));
+            serviceCollection.AddTransient(provider => new Lazy<IUserViewManager>(provider.GetRequiredService<IUserViewManager>));
+            serviceCollection.AddSingleton<ILibraryManager, LibraryManager>();
+            serviceCollection.AddSingleton<NamingOptions>();
 
-            ServiceCollection.AddSingleton<IMusicManager, MusicManager>();
+            serviceCollection.AddSingleton<IMusicManager, MusicManager>();
 
-            ServiceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
+            serviceCollection.AddSingleton<ILibraryMonitor, LibraryMonitor>();
 
-            ServiceCollection.AddSingleton<ISearchEngine, SearchEngine>();
+            serviceCollection.AddSingleton<ISearchEngine, SearchEngine>();
 
-            ServiceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
+            serviceCollection.AddSingleton<IWebSocketManager, WebSocketManager>();
 
-            ServiceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
+            serviceCollection.AddSingleton<IImageProcessor, ImageProcessor>();
 
-            ServiceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
+            serviceCollection.AddSingleton<ITVSeriesManager, TVSeriesManager>();
 
-            ServiceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
+            serviceCollection.AddSingleton<IMediaSourceManager, MediaSourceManager>();
 
-            ServiceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
+            serviceCollection.AddSingleton<ISubtitleManager, SubtitleManager>();
 
-            ServiceCollection.AddSingleton<IProviderManager, ProviderManager>();
+            serviceCollection.AddSingleton<IProviderManager, ProviderManager>();
 
             // TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
-            ServiceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
-            ServiceCollection.AddSingleton<IDtoService, DtoService>();
-
-            ServiceCollection.AddSingleton<IChannelManager, ChannelManager>();
+            serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>));
+            serviceCollection.AddSingleton<IDtoService, DtoService>();
 
-            ServiceCollection.AddSingleton<ISessionManager, SessionManager>();
+            serviceCollection.AddSingleton<IChannelManager, ChannelManager>();
 
-            ServiceCollection.AddSingleton<IDlnaManager, DlnaManager>();
+            serviceCollection.AddSingleton<ISessionManager, SessionManager>();
 
-            ServiceCollection.AddSingleton<ICollectionManager, CollectionManager>();
+            serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
 
-            ServiceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
+            serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
 
-            ServiceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
+            serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
 
-            ServiceCollection.AddSingleton<LiveTvDtoService>();
-            ServiceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
+            serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>();
 
-            ServiceCollection.AddSingleton<IUserViewManager, UserViewManager>();
+            serviceCollection.AddSingleton<LiveTvDtoService>();
+            serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>();
 
-            ServiceCollection.AddSingleton<INotificationManager, NotificationManager>();
+            serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
 
-            ServiceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
+            serviceCollection.AddSingleton<INotificationManager, NotificationManager>();
 
-            ServiceCollection.AddSingleton<IChapterManager, ChapterManager>();
+            serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
 
-            ServiceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
+            serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
 
-            ServiceCollection.AddScoped<ISessionContext, SessionContext>();
+            serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
 
-            ServiceCollection.AddSingleton<IAuthService, AuthService>();
-            ServiceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
+            serviceCollection.AddScoped<ISessionContext, SessionContext>();
 
-            ServiceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
+            serviceCollection.AddSingleton<IAuthService, AuthService>();
+            serviceCollection.AddSingleton<IQuickConnect, QuickConnectManager>();
 
-            ServiceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
+            serviceCollection.AddSingleton<ISubtitleEncoder, MediaBrowser.MediaEncoding.Subtitles.SubtitleEncoder>();
 
-            ServiceCollection.AddSingleton<TranscodingJobHelper>();
-            ServiceCollection.AddScoped<MediaInfoHelper>();
-            ServiceCollection.AddScoped<AudioHelper>();
-            ServiceCollection.AddScoped<DynamicHlsHelper>();
+            serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
 
-            ServiceCollection.AddSingleton<IDirectoryService, DirectoryService>();
+            serviceCollection.AddSingleton<TranscodingJobHelper>();
+            serviceCollection.AddScoped<MediaInfoHelper>();
+            serviceCollection.AddScoped<AudioHelper>();
+            serviceCollection.AddScoped<DynamicHlsHelper>();
+            serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>();
+            serviceCollection.AddSingleton<IDirectoryService, DirectoryService>();
         }
 
         /// <summary>
@@ -795,8 +783,6 @@ namespace Emby.Server.Implementations
 
             _pluginManager.CreatePlugins();
 
-            _urlPrefixes = GetUrlPrefixes().ToArray();
-
             Resolve<ILibraryManager>().AddParts(
                 GetExports<IResolverIgnoreRule>(),
                 GetExports<IItemResolver>(),
@@ -864,32 +850,12 @@ namespace Emby.Server.Implementations
             }
         }
 
-        private IEnumerable<string> GetUrlPrefixes()
-        {
-            var hosts = new[] { "+" };
-
-            return hosts.SelectMany(i =>
-            {
-                var prefixes = new List<string>
-                {
-                    "http://" + i + ":" + HttpPort + "/"
-                };
-
-                if (Certificate != null)
-                {
-                    prefixes.Add("https://" + i + ":" + HttpsPort + "/");
-                }
-
-                return prefixes;
-            });
-        }
-
         /// <summary>
         /// Called when [configuration updated].
         /// </summary>
         /// <param name="sender">The sender.</param>
         /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
-        protected void OnConfigurationUpdated(object sender, EventArgs e)
+        private void OnConfigurationUpdated(object sender, EventArgs e)
         {
             var requiresRestart = false;
             var networkConfiguration = ConfigurationManager.GetNetworkConfiguration();
@@ -898,8 +864,8 @@ namespace Emby.Server.Implementations
             if (HttpPort != 0 && HttpsPort != 0)
             {
                 // Need to restart if ports have changed
-                if (networkConfiguration.HttpServerPortNumber != HttpPort ||
-                    networkConfiguration.HttpsPortNumber != HttpsPort)
+                if (networkConfiguration.HttpServerPortNumber != HttpPort
+                    || networkConfiguration.HttpsPortNumber != HttpsPort)
                 {
                     if (ConfigurationManager.Configuration.IsPortAuthorized)
                     {
@@ -911,11 +877,6 @@ namespace Emby.Server.Implementations
                 }
             }
 
-            if (!_urlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase))
-            {
-                requiresRestart = true;
-            }
-
             if (ValidateSslCertificate(networkConfiguration))
             {
                 requiresRestart = true;
@@ -957,7 +918,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <summary>
-        /// Notifies that the kernel that a change has been made that requires a restart.
+        /// Notifies the kernel that a change has been made that requires a restart.
         /// </summary>
         public void NotifyPendingRestart()
         {
@@ -1098,11 +1059,6 @@ namespace Emby.Server.Implementations
             };
         }
 
-        public IEnumerable<WakeOnLanInfo> GetWakeOnLanInfo()
-            => NetManager.GetMacAddresses()
-                .Select(i => new WakeOnLanInfo(i))
-                .ToList();
-
         public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
         {
             return new PublicSystemInfo
@@ -1118,7 +1074,7 @@ namespace Emby.Server.Implementations
         }
 
         /// <inheritdoc/>
-        public string GetSmartApiUrl(IPAddress remoteAddr, int? port = null)
+        public string GetSmartApiUrl(IPAddress remoteAddr)
         {
             // Published server ends with a /
             if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1127,18 +1083,12 @@ namespace Emby.Server.Implementations
                 return PublishedServerUrl.Trim('/');
             }
 
-            string smart = NetManager.GetBindInterface(remoteAddr, out port);
-            // If the smartAPI doesn't start with http then treat it as a host or ip.
-            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                return smart.Trim('/');
-            }
-
+            string smart = NetManager.GetBindInterface(remoteAddr, out var port);
             return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
 
         /// <inheritdoc/>
-        public string GetSmartApiUrl(HttpRequest request, int? port = null)
+        public string GetSmartApiUrl(HttpRequest request)
         {
             // Return the host in the HTTP request as the API url
             if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
@@ -1159,18 +1109,12 @@ namespace Emby.Server.Implementations
                 return PublishedServerUrl.Trim('/');
             }
 
-            string smart = NetManager.GetBindInterface(request, out port);
-            // If the smartAPI doesn't start with http then treat it as a host or ip.
-            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                return smart.Trim('/');
-            }
-
+            string smart = NetManager.GetBindInterface(request, out var port);
             return GetLocalApiUrl(smart.Trim('/'), request.Scheme, port);
         }
 
         /// <inheritdoc/>
-        public string GetSmartApiUrl(string hostname, int? port = null)
+        public string GetSmartApiUrl(string hostname)
         {
             // Published server ends with a /
             if (!string.IsNullOrEmpty(PublishedServerUrl))
@@ -1179,31 +1123,29 @@ namespace Emby.Server.Implementations
                 return PublishedServerUrl.Trim('/');
             }
 
-            string smart = NetManager.GetBindInterface(hostname, out port);
-
-            // If the smartAPI doesn't start with http then treat it as a host or ip.
-            if (smart.StartsWith("http", StringComparison.OrdinalIgnoreCase))
-            {
-                return smart.Trim('/');
-            }
-
+            string smart = NetManager.GetBindInterface(hostname, out var port);
             return GetLocalApiUrl(smart.Trim('/'), null, port);
         }
 
         /// <inheritdoc/>
-        public string GetLoopbackHttpApiUrl()
+        public string GetApiUrlForLocalAccess(bool allowHttps = true)
         {
-            if (NetManager.IsIP6Enabled)
-            {
-                return GetLocalApiUrl("::1", Uri.UriSchemeHttp, HttpPort);
-            }
-
-            return GetLocalApiUrl("127.0.0.1", Uri.UriSchemeHttp, HttpPort);
+            // With an empty source, the port will be null
+            string smart = NetManager.GetBindInterface(string.Empty, out _);
+            var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
+            int? port = !allowHttps ? HttpPort : null;
+            return GetLocalApiUrl(smart.Trim('/'), scheme, port);
         }
 
         /// <inheritdoc/>
         public string GetLocalApiUrl(string hostname, string scheme = null, int? port = null)
         {
+            // If the smartAPI doesn't start with http then treat it as a host or ip.
+            if (hostname.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+            {
+                return hostname.TrimEnd('/');
+            }
+
             // NOTE: If no BaseUrl is set then UriBuilder appends a trailing slash, but if there is no BaseUrl it does
             // not. For consistency, always trim the trailing slash.
             return new UriBuilder
@@ -1277,12 +1219,15 @@ namespace Emby.Server.Implementations
 
                 Logger.LogInformation("Disposing {Type}", type.Name);
 
-                var parts = _disposableParts.Distinct().Where(i => i.GetType() != type).ToList();
-                _disposableParts.Clear();
-
-                foreach (var part in parts)
+                foreach (var (part, _) in _disposableParts)
                 {
-                    Logger.LogInformation("Disposing {Type}", part.GetType().Name);
+                    var partType = part.GetType();
+                    if (partType == type)
+                    {
+                        continue;
+                    }
+
+                    Logger.LogInformation("Disposing {Type}", partType.Name);
 
                     try
                     {
@@ -1290,9 +1235,11 @@ namespace Emby.Server.Implementations
                     }
                     catch (Exception ex)
                     {
-                        Logger.LogError(ex, "Error disposing {Type}", part.GetType().Name);
+                        Logger.LogError(ex, "Error disposing {Type}", partType.Name);
                     }
                 }
+
+                _disposableParts.Clear();
             }
 
             _disposed = true;

+ 0 - 8
Emby.Server.Implementations/Channels/ChannelManager.cs

@@ -1075,14 +1075,6 @@ namespace Emby.Server.Implementations.Channels
                 forceUpdate = true;
             }
 
-            // was used for status
-            // if (!string.Equals(item.ExternalEtag ?? string.Empty, info.Etag ?? string.Empty, StringComparison.Ordinal))
-            // {
-            //    item.ExternalEtag = info.Etag;
-            //    forceUpdate = true;
-            //    _logger.LogDebug("Forcing update due to ExternalEtag {0}", item.Name);
-            // }
-
             if (!internalChannelId.Equals(item.ChannelId))
             {
                 forceUpdate = true;

+ 55 - 35
Emby.Server.Implementations/Cryptography/CryptographyProvider.cs

@@ -1,9 +1,11 @@
 using System;
 using System.Collections.Generic;
+using System.Globalization;
 using System.Security.Cryptography;
+using System.Text;
 using MediaBrowser.Common.Extensions;
 using MediaBrowser.Model.Cryptography;
-using static MediaBrowser.Common.Cryptography.Constants;
+using static MediaBrowser.Model.Cryptography.Constants;
 
 namespace Emby.Server.Implementations.Cryptography
 {
@@ -12,10 +14,7 @@ namespace Emby.Server.Implementations.Cryptography
     /// </summary>
     public class CryptographyProvider : ICryptoProvider
     {
-        // FIXME: When we get DotNet Standard 2.1 we need to revisit how we do the crypto
-        // Currently supported hash methods from https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptoconfig?view=netcore-2.1
-        // there might be a better way to autogenerate this list as dotnet updates, but I couldn't find one
-        // Please note the default method of PBKDF2 is not included, it cannot be used to generate hashes cleanly as it is actually a pbkdf with sha1
+        // TODO: remove when not needed for backwards compat
         private static readonly HashSet<string> _supportedHashMethods = new HashSet<string>()
             {
                 "MD5",
@@ -35,60 +34,81 @@ namespace Emby.Server.Implementations.Cryptography
             };
 
         /// <inheritdoc />
-        public string DefaultHashMethod => "PBKDF2";
+        public string DefaultHashMethod => "PBKDF2-SHA512";
 
         /// <inheritdoc />
-        public IEnumerable<string> GetSupportedHashMethods()
-            => _supportedHashMethods;
-
-        private byte[] PBKDF2(string method, byte[] bytes, byte[] salt, int iterations)
+        public PasswordHash CreatePasswordHash(ReadOnlySpan<char> password)
         {
-            // downgrading for now as we need this library to be dotnetstandard compliant
-            // with this downgrade we'll add a check to make sure we're on the downgrade method at the moment
-            if (method != DefaultHashMethod)
-            {
-                throw new CryptographicException($"Cannot currently use PBKDF2 with requested hash method: {method}");
-            }
-
-            using var r = new Rfc2898DeriveBytes(bytes, salt, iterations);
-            return r.GetBytes(32);
+            byte[] salt = GenerateSalt();
+            return new PasswordHash(
+                DefaultHashMethod,
+                Rfc2898DeriveBytes.Pbkdf2(
+                    password,
+                    salt,
+                    DefaultIterations,
+                    HashAlgorithmName.SHA512,
+                    DefaultOutputLength),
+                salt,
+                new Dictionary<string, string>
+                {
+                    { "iterations", DefaultIterations.ToString(CultureInfo.InvariantCulture) }
+                });
         }
 
         /// <inheritdoc />
-        public byte[] ComputeHash(string hashMethod, byte[] bytes, byte[] salt)
+        public bool Verify(PasswordHash hash, ReadOnlySpan<char> password)
         {
-            if (hashMethod == DefaultHashMethod)
+            if (string.Equals(hash.Id, "PBKDF2", StringComparison.Ordinal))
             {
-                return PBKDF2(hashMethod, bytes, salt, DefaultIterations);
+                return hash.Hash.SequenceEqual(
+                    Rfc2898DeriveBytes.Pbkdf2(
+                        password,
+                        hash.Salt,
+                        int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+                        HashAlgorithmName.SHA1,
+                        32));
             }
 
-            if (!_supportedHashMethods.Contains(hashMethod))
+            if (string.Equals(hash.Id, "PBKDF2-SHA512", StringComparison.Ordinal))
             {
-                throw new CryptographicException($"Requested hash method is not supported: {hashMethod}");
+                return hash.Hash.SequenceEqual(
+                    Rfc2898DeriveBytes.Pbkdf2(
+                        password,
+                        hash.Salt,
+                        int.Parse(hash.Parameters["iterations"], CultureInfo.InvariantCulture),
+                        HashAlgorithmName.SHA512,
+                        DefaultOutputLength));
             }
 
-            using var h = HashAlgorithm.Create(hashMethod) ?? throw new ResourceNotFoundException($"Unknown hash method: {hashMethod}.");
-            if (salt.Length == 0)
+            if (!_supportedHashMethods.Contains(hash.Id))
             {
-                return h.ComputeHash(bytes);
+                throw new CryptographicException($"Requested hash method is not supported: {hash.Id}");
             }
 
-            byte[] salted = new byte[bytes.Length + salt.Length];
+            using var h = HashAlgorithm.Create(hash.Id) ?? throw new ResourceNotFoundException($"Unknown hash method: {hash.Id}.");
+            var bytes = Encoding.UTF8.GetBytes(password.ToArray());
+            if (hash.Salt.Length == 0)
+            {
+                return hash.Hash.SequenceEqual(h.ComputeHash(bytes));
+            }
+
+            byte[] salted = new byte[bytes.Length + hash.Salt.Length];
             Array.Copy(bytes, salted, bytes.Length);
-            Array.Copy(salt, 0, salted, bytes.Length, salt.Length);
-            return h.ComputeHash(salted);
+            hash.Salt.CopyTo(salted.AsSpan(bytes.Length));
+            return hash.Hash.SequenceEqual(h.ComputeHash(salted));
         }
 
-        /// <inheritdoc />
-        public byte[] ComputeHashWithDefaultMethod(byte[] bytes, byte[] salt)
-            => PBKDF2(DefaultHashMethod, bytes, salt, DefaultIterations);
-
         /// <inheritdoc />
         public byte[] GenerateSalt()
             => GenerateSalt(DefaultSaltLength);
 
         /// <inheritdoc />
         public byte[] GenerateSalt(int length)
-            => RandomNumberGenerator.GetBytes(length);
+        {
+            var salt = new byte[length];
+            using var rng = RandomNumberGenerator.Create();
+            rng.GetNonZeroBytes(salt);
+            return salt;
+        }
     }
 }

+ 1 - 1
Emby.Server.Implementations/Data/ManagedConnection.cs

@@ -7,7 +7,7 @@ using SQLitePCL.pretty;
 
 namespace Emby.Server.Implementations.Data
 {
-    public class ManagedConnection : IDisposable
+    public sealed class ManagedConnection : IDisposable
     {
         private readonly SemaphoreSlim _writeLock;
 

+ 9 - 15
Emby.Server.Implementations/Dto/DtoService.cs

@@ -134,14 +134,11 @@ namespace Emby.Server.Implementations.Dto
             var dto = GetBaseItemDtoInternal(item, options, user, owner);
             if (item is LiveTvChannel tvChannel)
             {
-                var list = new List<(BaseItemDto, LiveTvChannel)>(1) { (dto, tvChannel) };
-                LivetvManager.AddChannelInfo(list, options, user);
+                LivetvManager.AddChannelInfo(new[] { (dto, tvChannel) }, options, user);
             }
             else if (item is LiveTvProgram)
             {
-                var list = new List<(BaseItem, BaseItemDto)>(1) { (item, dto) };
-                var task = LivetvManager.AddInfoToProgramDto(list, options.Fields, user);
-                Task.WaitAll(task);
+                LivetvManager.AddInfoToProgramDto(new[] { (item, dto) }, options.Fields, user).GetAwaiter().GetResult();
             }
 
             if (item is IItemByName itemByName
@@ -373,6 +370,12 @@ namespace Emby.Server.Implementations.Dto
                     if (item is MusicAlbum || item is Season || item is Playlist)
                     {
                         dto.ChildCount = dto.RecursiveItemCount;
+                        var folderChildCount = folder.LinkedChildren.Length;
+                        // The default is an empty array, so we can't reliably use the count when it's empty
+                        if (folderChildCount > 0)
+                        {
+                            dto.ChildCount ??= folderChildCount;
+                        }
                     }
 
                     if (options.ContainsField(ItemFields.ChildCount))
@@ -497,7 +500,7 @@ namespace Emby.Server.Implementations.Dto
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path);
+                _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path);
                 return null;
             }
         }
@@ -755,15 +758,6 @@ namespace Emby.Server.Implementations.Dto
                 dto.BackdropImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Backdrop, backdropLimit);
             }
 
-            if (options.ContainsField(ItemFields.ScreenshotImageTags))
-            {
-                var screenshotLimit = options.GetImageLimit(ImageType.Screenshot);
-                if (screenshotLimit > 0)
-                {
-                    dto.ScreenshotImageTags = GetTagsAndFillBlurhashes(dto, item, ImageType.Screenshot, screenshotLimit);
-                }
-            }
-
             if (options.ContainsField(ItemFields.Genres))
             {
                 dto.Genres = item.Genres;

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

@@ -25,14 +25,14 @@
   <ItemGroup>
     <PackageReference Include="DiscUtils.Udf" Version="0.16.13" />
     <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
-    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" />
-    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" />
-    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" />
-    <PackageReference Include="Mono.Nat" Version="3.0.1" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
+    <PackageReference Include="Mono.Nat" Version="3.0.2" />
     <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" />
-    <PackageReference Include="sharpcompress" Version="0.30.0" />
+    <PackageReference Include="sharpcompress" Version="0.30.1" />
     <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
     <PackageReference Include="DotNet.Glob" Version="3.1.3" />
   </ItemGroup>

+ 1 - 5
Emby.Server.Implementations/EntryPoints/ExternalPortForwarding.cs

@@ -27,7 +27,6 @@ namespace Emby.Server.Implementations.EntryPoints
         private readonly IServerApplicationHost _appHost;
         private readonly ILogger<ExternalPortForwarding> _logger;
         private readonly IServerConfigurationManager _config;
-        private readonly IDeviceDiscovery _deviceDiscovery;
 
         private readonly ConcurrentDictionary<IPEndPoint, byte> _createdRules = new ConcurrentDictionary<IPEndPoint, byte>();
 
@@ -42,17 +41,14 @@ namespace Emby.Server.Implementations.EntryPoints
         /// <param name="logger">The logger.</param>
         /// <param name="appHost">The application host.</param>
         /// <param name="config">The configuration manager.</param>
-        /// <param name="deviceDiscovery">The device discovery.</param>
         public ExternalPortForwarding(
             ILogger<ExternalPortForwarding> logger,
             IServerApplicationHost appHost,
-            IServerConfigurationManager config,
-            IDeviceDiscovery deviceDiscovery)
+            IServerConfigurationManager config)
         {
             _logger = logger;
             _appHost = appHost;
             _config = config;
-            _deviceDiscovery = deviceDiscovery;
         }
 
         private string GetConfigIdentifier()

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

@@ -24,7 +24,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
 
             if (!auth.HasToken)
             {
-                throw new AuthenticationException("Request does not contain a token.");
+                return auth;
             }
 
             if (!auth.IsAuthenticated)

+ 6 - 1
Emby.Server.Implementations/HttpServer/WebSocketManager.cs

@@ -35,7 +35,12 @@ namespace Emby.Server.Implementations.HttpServer
         /// <inheritdoc />
         public async Task WebSocketRequestHandler(HttpContext context)
         {
-            _ = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+            var authorizationInfo = await _authService.Authenticate(context.Request).ConfigureAwait(false);
+            if (!authorizationInfo.IsAuthenticated)
+            {
+                throw new SecurityException("Token is required");
+            }
+
             try
             {
                 _logger.LogInformation("WS {IP} request", context.Connection.RemoteIpAddress);

+ 11 - 13
Emby.Server.Implementations/IO/FileRefresher.cs

@@ -1,5 +1,3 @@
-#nullable disable
-
 #pragma warning disable CS1591
 
 using System;
@@ -14,7 +12,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Emby.Server.Implementations.IO
 {
-    public class FileRefresher : IDisposable
+    public sealed class FileRefresher : IDisposable
     {
         private readonly ILogger _logger;
         private readonly ILibraryManager _libraryManager;
@@ -22,7 +20,7 @@ namespace Emby.Server.Implementations.IO
 
         private readonly List<string> _affectedPaths = new List<string>();
         private readonly object _timerLock = new object();
-        private Timer _timer;
+        private Timer? _timer;
         private bool _disposed;
 
         public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger)
@@ -36,7 +34,7 @@ namespace Emby.Server.Implementations.IO
             AddPath(path);
         }
 
-        public event EventHandler<EventArgs> Completed;
+        public event EventHandler<EventArgs>? Completed;
 
         public string Path { get; private set; }
 
@@ -111,7 +109,7 @@ namespace Emby.Server.Implementations.IO
             RestartTimer();
         }
 
-        private void OnTimerCallback(object state)
+        private void OnTimerCallback(object? state)
         {
             List<string> paths;
 
@@ -127,7 +125,7 @@ namespace Emby.Server.Implementations.IO
 
             try
             {
-                ProcessPathChanges(paths.ToList());
+                ProcessPathChanges(paths);
             }
             catch (Exception ex)
             {
@@ -137,12 +135,12 @@ namespace Emby.Server.Implementations.IO
 
         private void ProcessPathChanges(List<string> paths)
         {
-            var itemsToRefresh = paths
+            IEnumerable<BaseItem> itemsToRefresh = paths
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Select(GetAffectedBaseItem)
                 .Where(item => item != null)
-                .GroupBy(x => x.Id)
-                .Select(x => x.First());
+                .GroupBy(x => x!.Id) // Removed null values in the previous .Where()
+                .Select(x => x.First())!;
 
             foreach (var item in itemsToRefresh)
             {
@@ -176,15 +174,15 @@ namespace Emby.Server.Implementations.IO
         /// </summary>
         /// <param name="path">The path.</param>
         /// <returns>BaseItem.</returns>
-        private BaseItem GetAffectedBaseItem(string path)
+        private BaseItem? GetAffectedBaseItem(string path)
         {
-            BaseItem item = null;
+            BaseItem? item = null;
 
             while (item == null && !string.IsNullOrEmpty(path))
             {
                 item = _libraryManager.FindByPath(path, null);
 
-                path = System.IO.Path.GetDirectoryName(path);
+                path = System.IO.Path.GetDirectoryName(path) ?? string.Empty;
             }
 
             if (item != null)

+ 6 - 4
Emby.Server.Implementations/IO/LibraryMonitor.cs

@@ -267,7 +267,7 @@ namespace Emby.Server.Implementations.IO
                     if (_fileSystemWatchers.TryAdd(path, newWatcher))
                     {
                         newWatcher.EnableRaisingEvents = true;
-                        _logger.LogInformation("Watching directory " + path);
+                        _logger.LogInformation("Watching directory {Path}", path);
                     }
                     else
                     {
@@ -276,7 +276,7 @@ namespace Emby.Server.Implementations.IO
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error watching path: {path}", path);
+                    _logger.LogError(ex, "Error watching path: {Path}", path);
                 }
             });
         }
@@ -449,12 +449,12 @@ namespace Emby.Server.Implementations.IO
                 }
 
                 var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
-                newRefresher.Completed += NewRefresher_Completed;
+                newRefresher.Completed += OnNewRefresherCompleted;
                 _activeRefreshers.Add(newRefresher);
             }
         }
 
-        private void NewRefresher_Completed(object sender, EventArgs e)
+        private void OnNewRefresherCompleted(object sender, EventArgs e)
         {
             var refresher = (FileRefresher)sender;
             DisposeRefresher(refresher);
@@ -481,6 +481,7 @@ namespace Emby.Server.Implementations.IO
         {
             lock (_activeRefreshers)
             {
+                refresher.Completed -= OnNewRefresherCompleted;
                 refresher.Dispose();
                 _activeRefreshers.Remove(refresher);
             }
@@ -492,6 +493,7 @@ namespace Emby.Server.Implementations.IO
             {
                 foreach (var refresher in _activeRefreshers.ToList())
                 {
+                    refresher.Completed -= OnNewRefresherCompleted;
                     refresher.Dispose();
                 }
 

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

@@ -1,5 +1,3 @@
-#pragma warning disable CS1591
-
 using System;
 using System.Collections.Generic;
 using System.Globalization;
@@ -23,6 +21,11 @@ namespace Emby.Server.Implementations.IO
         private readonly string _tempPath;
         private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
+        /// </summary>
+        /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
+        /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
         public ManagedFileSystem(
             ILogger<ManagedFileSystem> logger,
             IApplicationPaths applicationPaths)
@@ -31,6 +34,7 @@ namespace Emby.Server.Implementations.IO
             _tempPath = applicationPaths.TempDirectory;
         }
 
+        /// <inheritdoc />
         public virtual void AddShortcutHandler(IShortcutHandler handler)
         {
             _shortcutHandlers.Add(handler);
@@ -72,6 +76,7 @@ namespace Emby.Server.Implementations.IO
             return handler?.Resolve(filename);
         }
 
+        /// <inheritdoc />
         public virtual string MakeAbsolutePath(string folderPath, string filePath)
         {
             // path is actually a stream
@@ -358,11 +363,13 @@ namespace Emby.Server.Implementations.IO
             return GetCreationTimeUtc(GetFileSystemInfo(path));
         }
 
+        /// <inheritdoc />
         public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info)
         {
             return info.CreationTimeUtc;
         }
 
+        /// <inheritdoc />
         public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info)
         {
             return info.LastWriteTimeUtc;
@@ -397,6 +404,7 @@ namespace Emby.Server.Implementations.IO
             return GetLastWriteTimeUtc(GetFileSystemInfo(path));
         }
 
+        /// <inheritdoc />
         public virtual void SetHidden(string path, bool isHidden)
         {
             if (!OperatingSystem.IsWindows())
@@ -421,6 +429,7 @@ namespace Emby.Server.Implementations.IO
             }
         }
 
+        /// <inheritdoc />
         public virtual void SetAttributes(string path, bool isHidden, bool readOnly)
         {
             if (!OperatingSystem.IsWindows())
@@ -444,7 +453,7 @@ namespace Emby.Server.Implementations.IO
 
             if (readOnly)
             {
-                attributes = attributes | FileAttributes.ReadOnly;
+                attributes |= FileAttributes.ReadOnly;
             }
             else
             {
@@ -453,7 +462,7 @@ namespace Emby.Server.Implementations.IO
 
             if (isHidden)
             {
-                attributes = attributes | FileAttributes.Hidden;
+                attributes |= FileAttributes.Hidden;
             }
             else
             {
@@ -498,6 +507,7 @@ namespace Emby.Server.Implementations.IO
             File.Copy(temp1, file2, true);
         }
 
+        /// <inheritdoc />
         public virtual bool ContainsSubPath(string parentPath, string path)
         {
             if (string.IsNullOrEmpty(parentPath))
@@ -515,6 +525,7 @@ namespace Emby.Server.Implementations.IO
                 _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
         }
 
+        /// <inheritdoc />
         public virtual string NormalizePath(string path)
         {
             if (string.IsNullOrEmpty(path))
@@ -530,6 +541,7 @@ namespace Emby.Server.Implementations.IO
             return Path.TrimEndingDirectorySeparator(path);
         }
 
+        /// <inheritdoc />
         public virtual bool AreEqual(string path1, string path2)
         {
             if (path1 == null && path2 == null)
@@ -548,6 +560,7 @@ namespace Emby.Server.Implementations.IO
                 _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
         }
 
+        /// <inheritdoc />
         public virtual string GetFileNameWithoutExtension(FileSystemMetadata info)
         {
             if (info.IsDirectory)
@@ -558,11 +571,11 @@ namespace Emby.Server.Implementations.IO
             return Path.GetFileNameWithoutExtension(info.FullName);
         }
 
+        /// <inheritdoc />
         public virtual bool IsPathFile(string path)
         {
-            // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\
-            if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 &&
-                !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
+            if (path.Contains("://", StringComparison.OrdinalIgnoreCase)
+                && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
             {
                 return false;
             }
@@ -570,17 +583,23 @@ namespace Emby.Server.Implementations.IO
             return true;
         }
 
+        /// <inheritdoc />
         public virtual void DeleteFile(string path)
         {
             SetAttributes(path, false, false);
             File.Delete(path);
         }
 
+        /// <inheritdoc />
         public virtual List<FileSystemMetadata> GetDrives()
         {
             // check for ready state to avoid waiting for drives to timeout
             // some drives on linux have no actual size or are used for other purposes
-            return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram)
+            return DriveInfo.GetDrives()
+                .Where(
+                    d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable)
+                        && d.IsReady
+                        && d.TotalSize != 0)
                 .Select(d => new FileSystemMetadata
                 {
                     Name = d.Name,
@@ -589,16 +608,19 @@ namespace Emby.Server.Implementations.IO
                 }).ToList();
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false)
         {
             return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive)));
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false)
         {
             return GetFiles(path, null, false, recursive);
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
             var enumerationOptions = GetEnumerationOptions(recursive);
@@ -629,6 +651,7 @@ namespace Emby.Server.Implementations.IO
             return ToMetadata(files);
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false)
         {
             var directoryInfo = new DirectoryInfo(path);
@@ -642,16 +665,19 @@ namespace Emby.Server.Implementations.IO
             return infos.Select(GetFileSystemMetadata);
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false)
         {
             return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive));
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false)
         {
             return GetFilePaths(path, null, false, recursive);
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false)
         {
             var enumerationOptions = GetEnumerationOptions(recursive);
@@ -682,6 +708,7 @@ namespace Emby.Server.Implementations.IO
             return files;
         }
 
+        /// <inheritdoc />
         public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false)
         {
             return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive));

+ 7 - 6
Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs

@@ -1,8 +1,9 @@
 using System;
 using System.IO;
+using Emby.Naming.Audio;
+using Emby.Naming.Common;
 using MediaBrowser.Controller;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
 using MediaBrowser.Model.IO;
 
@@ -13,17 +14,17 @@ namespace Emby.Server.Implementations.Library
     /// </summary>
     public class CoreResolutionIgnoreRule : IResolverIgnoreRule
     {
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
         private readonly IServerApplicationPaths _serverApplicationPaths;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="CoreResolutionIgnoreRule"/> class.
         /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <param name="serverApplicationPaths">The server application paths.</param>
-        public CoreResolutionIgnoreRule(ILibraryManager libraryManager, IServerApplicationPaths serverApplicationPaths)
+        public CoreResolutionIgnoreRule(NamingOptions namingOptions, IServerApplicationPaths serverApplicationPaths)
         {
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
             _serverApplicationPaths = serverApplicationPaths;
         }
 
@@ -78,7 +79,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     // Don't resolve these into audio files
                     if (Path.GetFileNameWithoutExtension(filename.AsSpan()).Equals(BaseItem.ThemeSongFileName, StringComparison.Ordinal)
-                        && _libraryManager.IsAudioFile(filename))
+                        && AudioFileParser.IsAudioFile(filename, _namingOptions))
                     {
                         return true;
                     }

+ 21 - 43
Emby.Server.Implementations/Library/LibraryManager.cs

@@ -79,6 +79,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IFileSystem _fileSystem;
         private readonly IItemRepository _itemRepository;
         private readonly IImageProcessor _imageProcessor;
+        private readonly NamingOptions _namingOptions;
 
         /// <summary>
         /// The _root folder sync lock.
@@ -88,9 +89,6 @@ namespace Emby.Server.Implementations.Library
 
         private readonly TimeSpan _viewRefreshInterval = TimeSpan.FromHours(24);
 
-        private NamingOptions _namingOptions;
-        private string[] _videoFileExtensions;
-
         /// <summary>
         /// The _root folder.
         /// </summary>
@@ -116,6 +114,7 @@ namespace Emby.Server.Implementations.Library
         /// <param name="itemRepository">The item repository.</param>
         /// <param name="imageProcessor">The image processor.</param>
         /// <param name="memoryCache">The memory cache.</param>
+        /// <param name="namingOptions">The naming options.</param>
         public LibraryManager(
             IServerApplicationHost appHost,
             ILogger<LibraryManager> logger,
@@ -130,7 +129,8 @@ namespace Emby.Server.Implementations.Library
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepository,
             IImageProcessor imageProcessor,
-            IMemoryCache memoryCache)
+            IMemoryCache memoryCache,
+            NamingOptions namingOptions)
         {
             _appHost = appHost;
             _logger = logger;
@@ -146,6 +146,7 @@ namespace Emby.Server.Implementations.Library
             _itemRepository = itemRepository;
             _imageProcessor = imageProcessor;
             _memoryCache = memoryCache;
+            _namingOptions = namingOptions;
 
             _configurationManager.ConfigurationUpdated += ConfigurationUpdated;
 
@@ -333,8 +334,7 @@ namespace Emby.Server.Implementations.Library
                 {
                     try
                     {
-                        var task = BaseItem.ChannelManager.DeleteItem(item);
-                        Task.WaitAll(task);
+                        BaseItem.ChannelManager.DeleteItem(item).GetAwaiter().GetResult();
                     }
                     catch (ArgumentException)
                     {
@@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.Library
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path);
+                _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path);
                 return null;
             }
         }
@@ -799,7 +799,7 @@ namespace Emby.Server.Implementations.Library
                     {
                         var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
 
-                        _logger.LogDebug("Creating userRootPath at {path}", userRootPath);
+                        _logger.LogDebug("Creating userRootPath at {Path}", userRootPath);
                         Directory.CreateDirectory(userRootPath);
 
                         var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder));
@@ -810,7 +810,7 @@ namespace Emby.Server.Implementations.Library
                         }
                         catch (Exception ex)
                         {
-                            _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId);
+                            _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId);
                         }
 
                         if (tmpItem == null)
@@ -827,7 +827,7 @@ namespace Emby.Server.Implementations.Library
                         }
 
                         _userRootFolder = tmpItem;
-                        _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder);
+                        _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder);
                     }
                 }
             }
@@ -1213,7 +1213,7 @@ namespace Emby.Server.Implementations.Library
                         }
                         catch (Exception ex)
                         {
-                            _logger.LogError(ex, "Error resolving shortcut file {file}", i);
+                            _logger.LogError(ex, "Error resolving shortcut file {File}", i);
                             return null;
                         }
                     })
@@ -1698,7 +1698,7 @@ namespace Emby.Server.Implementations.Library
 
                     if (video == null)
                     {
-                        _logger.LogError("Intro resolver returned null for {path}.", info.Path);
+                        _logger.LogError("Intro resolver returned null for {Path}.", info.Path);
                     }
                     else
                     {
@@ -1717,7 +1717,7 @@ namespace Emby.Server.Implementations.Library
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error resolving path {path}.", info.Path);
+                    _logger.LogError(ex, "Error resolving path {Path}.", info.Path);
                 }
             }
             else
@@ -2501,16 +2501,6 @@ namespace Emby.Server.Implementations.Library
             return RootFolder;
         }
 
-        /// <inheritdoc />
-        public bool IsVideoFile(string path)
-        {
-            return VideoResolver.IsVideoFile(path, GetNamingOptions());
-        }
-
-        /// <inheritdoc />
-        public bool IsAudioFile(string path)
-            => AudioFileParser.IsAudioFile(path, GetNamingOptions());
-
         /// <inheritdoc />
         public int? GetSeasonNumberFromPath(string path)
             => SeasonPathParser.Parse(path, true, true).SeasonNumber;
@@ -2526,7 +2516,7 @@ namespace Emby.Server.Implementations.Library
                 isAbsoluteNaming = null;
             }
 
-            var resolver = new EpisodeResolver(GetNamingOptions());
+            var resolver = new EpisodeResolver(_namingOptions);
 
             var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd;
 
@@ -2683,21 +2673,9 @@ namespace Emby.Server.Implementations.Library
             return changed;
         }
 
-        /// <inheritdoc />
-        public NamingOptions GetNamingOptions()
-        {
-            if (_namingOptions == null)
-            {
-                _namingOptions = new NamingOptions();
-                _videoFileExtensions = _namingOptions.VideoFileExtensions;
-            }
-
-            return _namingOptions;
-        }
-
         public ItemLookupInfo ParseName(string name)
         {
-            var namingOptions = GetNamingOptions();
+            var namingOptions = _namingOptions;
             var result = VideoResolver.CleanDateTime(name, namingOptions);
 
             return new ItemLookupInfo
@@ -2709,11 +2687,11 @@ namespace Emby.Server.Implementations.Library
 
         public IEnumerable<Video> FindTrailers(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
-            var namingOptions = GetNamingOptions();
+            var namingOptions = _namingOptions;
 
             var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
                 .Where(i => string.Equals(i.Name, BaseItem.TrailersFolderName, StringComparison.OrdinalIgnoreCase))
-                .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
+                .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
                 .ToList();
 
             var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
@@ -2727,7 +2705,7 @@ namespace Emby.Server.Implementations.Library
 
             var resolvers = new IItemResolver[]
             {
-                new GenericVideoResolver<Trailer>(this)
+                new GenericVideoResolver<Trailer>(_namingOptions)
             };
 
             return ResolvePaths(files, directoryService, null, new LibraryOptions(), null, resolvers)
@@ -2753,11 +2731,11 @@ namespace Emby.Server.Implementations.Library
 
         public IEnumerable<Video> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService)
         {
-            var namingOptions = GetNamingOptions();
+            var namingOptions = _namingOptions;
 
             var files = owner.IsInMixedFolder ? new List<FileSystemMetadata>() : fileSystemChildren.Where(i => i.IsDirectory)
                 .Where(i => BaseItem.AllExtrasTypesFolderNames.ContainsKey(i.Name ?? string.Empty))
-                .SelectMany(i => _fileSystem.GetFiles(i.FullName, _videoFileExtensions, false, false))
+                .SelectMany(i => _fileSystem.GetFiles(i.FullName, namingOptions.VideoFileExtensions, false, false))
                 .ToList();
 
             var videos = VideoListResolver.Resolve(fileSystemChildren, namingOptions);
@@ -2841,7 +2819,7 @@ namespace Emby.Server.Implementations.Library
 
         private void SetExtraTypeFromFilename(Video item)
         {
-            var resolver = new ExtraResolver(GetNamingOptions());
+            var resolver = new ExtraResolver(_namingOptions);
 
             var result = resolver.GetExtraInfo(item.Path);
 

+ 13 - 24
Emby.Server.Implementations/Library/MediaSourceManager.cs

@@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Library
         private readonly IMediaEncoder _mediaEncoder;
         private readonly ILocalizationManager _localizationManager;
         private readonly IApplicationPaths _appPaths;
+        private readonly IDirectoryService _directoryService;
 
         private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase);
         private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1);
@@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.Library
             ILogger<MediaSourceManager> logger,
             IFileSystem fileSystem,
             IUserDataManager userDataManager,
-            IMediaEncoder mediaEncoder)
+            IMediaEncoder mediaEncoder,
+            IDirectoryService directoryService)
         {
             _itemRepo = itemRepo;
             _userManager = userManager;
@@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Library
             _mediaEncoder = mediaEncoder;
             _localizationManager = localizationManager;
             _appPaths = applicationPaths;
+            _directoryService = directoryService;
         }
 
         public void AddParts(IEnumerable<IMediaSourceProvider> providers)
@@ -106,16 +109,6 @@ namespace Emby.Server.Implementations.Library
             return false;
         }
 
-        public List<MediaStream> GetMediaStreams(string mediaSourceId)
-        {
-            var list = GetMediaStreams(new MediaStreamQuery
-            {
-                ItemId = new Guid(mediaSourceId)
-            });
-
-            return GetMediaStreamsForItem(list);
-        }
-
         public List<MediaStream> GetMediaStreams(Guid itemId)
         {
             var list = GetMediaStreams(new MediaStreamQuery
@@ -161,7 +154,7 @@ namespace Emby.Server.Implementations.Library
             if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video))
             {
                 await item.RefreshMetadata(
-                    new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+                    new MetadataRefreshOptions(_directoryService)
                     {
                         EnableRemoteContentProbe = true,
                         MetadataRefreshMode = MetadataRefreshMode.FullRefresh
@@ -212,6 +205,7 @@ namespace Emby.Server.Implementations.Library
             return SortMediaSources(list);
         }
 
+        /// <inheritdoc />>
         public MediaProtocol GetPathProtocol(string path)
         {
             if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase))
@@ -258,7 +252,7 @@ namespace Emby.Server.Implementations.Library
             {
                 if (path != null)
                 {
-                    if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1)
+                    if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase))
                     {
                         return false;
                     }
@@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.Library
             catch (Exception ex)
             {
                 _logger.LogError(ex, "Error getting media sources");
-                return new List<MediaSourceInfo>();
+                return Enumerable.Empty<MediaSourceInfo>();
             }
         }
 
@@ -494,14 +488,11 @@ namespace Emby.Server.Implementations.Library
                 _liveStreamSemaphore.Release();
             }
 
-            // TODO: Don't hardcode this
-            const bool isAudio = false;
-
             try
             {
                 if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing)
                 {
-                    AddMediaInfo(mediaSource, isAudio);
+                    AddMediaInfo(mediaSource);
                 }
                 else
                 {
@@ -509,14 +500,14 @@ namespace Emby.Server.Implementations.Library
                     string cacheKey = request.OpenToken;
 
                     await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths)
-                        .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken)
+                        .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken)
                         .ConfigureAwait(false);
                 }
             }
             catch (Exception ex)
             {
                 _logger.LogError(ex, "Error probing live tv stream");
-                AddMediaInfo(mediaSource, isAudio);
+                AddMediaInfo(mediaSource);
             }
 
             // TODO: @bond Fix
@@ -536,7 +527,7 @@ namespace Emby.Server.Implementations.Library
             return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider);
         }
 
-        private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio)
+        private static void AddMediaInfo(MediaSourceInfo mediaSource)
         {
             mediaSource.DefaultSubtitleStreamIndex = null;
 
@@ -855,9 +846,7 @@ namespace Emby.Server.Implementations.Library
             return (provider, keyId);
         }
 
-        /// <summary>
-        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
-        /// </summary>
+        /// <inheritdoc />
         public void Dispose()
         {
             Dispose(true);

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

@@ -6,7 +6,10 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using Emby.Naming.Audio;
 using Emby.Naming.AudioBook;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -21,11 +24,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     /// </summary>
     public class AudioResolver : ItemResolver<MediaBrowser.Controller.Entities.Audio.Audio>, IMultiItemResolver
     {
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
 
-        public AudioResolver(ILibraryManager libraryManager)
+        public AudioResolver(NamingOptions namingOptions)
         {
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
         }
 
         /// <summary>
@@ -40,7 +43,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             string collectionType,
             IDirectoryService directoryService)
         {
-            var result = ResolveMultipleInternal(parent, files, collectionType, directoryService);
+            var result = ResolveMultipleInternal(parent, files, collectionType);
 
             if (result != null)
             {
@@ -56,12 +59,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         private MultiItemResolverResult ResolveMultipleInternal(
             Folder parent,
             List<FileSystemMetadata> files,
-            string collectionType,
-            IDirectoryService directoryService)
+            string collectionType)
         {
             if (string.Equals(collectionType, CollectionType.Books, StringComparison.OrdinalIgnoreCase))
             {
-                return ResolveMultipleAudio<AudioBook>(parent, files, directoryService, false, collectionType, true);
+                return ResolveMultipleAudio(parent, files, true);
             }
 
             return null;
@@ -87,14 +89,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                     return null;
                 }
 
-                var files = args.FileSystemChildren
-                    .Where(i => !_libraryManager.IgnoreFile(i, args.Parent))
-                    .ToList();
-
-                return FindAudio<AudioBook>(args, args.Path, args.Parent, files, args.DirectoryService, collectionType, false);
+                return FindAudioBook(args, false);
             }
 
-            if (_libraryManager.IsAudioFile(args.Path))
+            if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
             {
                 var extension = Path.GetExtension(args.Path);
 
@@ -107,7 +105,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 var isMixedCollectionType = string.IsNullOrEmpty(collectionType);
 
                 // For conflicting extensions, give priority to videos
-                if (isMixedCollectionType && _libraryManager.IsVideoFile(args.Path))
+                if (isMixedCollectionType && VideoResolver.IsVideoFile(args.Path, _namingOptions))
                 {
                     return null;
                 }
@@ -141,29 +139,23 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             return null;
         }
 
-        private T FindAudio<T>(ItemResolveArgs args, string path, Folder parent, List<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, string collectionType, bool parseName)
-            where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
+        private AudioBook FindAudioBook(ItemResolveArgs args, bool parseName)
         {
             // TODO: Allow GetMultiDiscMovie in here
-            const bool supportsMultiVersion = false;
+            var result = ResolveMultipleAudio(args.Parent, args.GetActualFileSystemChildren(), parseName);
 
-            var result = ResolveMultipleAudio<T>(parent, fileSystemEntries, directoryService, supportsMultiVersion, collectionType, parseName) ??
-                new MultiItemResolverResult();
-
-            if (result.Items.Count == 1)
+            if (result == null || result.Items.Count != 1 || result.Items[0] is not AudioBook item)
             {
-                // If we were supporting this we'd be checking filesFromOtherItems
-                var item = (T)result.Items[0];
-                item.IsInMixedFolder = false;
-                item.Name = Path.GetFileName(item.ContainingFolderPath);
-                return item;
+                return null;
             }
 
-            return null;
+            // If we were supporting this we'd be checking filesFromOtherItems
+            item.IsInMixedFolder = false;
+            item.Name = Path.GetFileName(item.ContainingFolderPath);
+            return item;
         }
 
-        private MultiItemResolverResult ResolveMultipleAudio<T>(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, IDirectoryService directoryService, bool suppportMultiEditions, string collectionType, bool parseName)
-            where T : MediaBrowser.Controller.Entities.Audio.Audio, new()
+        private MultiItemResolverResult ResolveMultipleAudio(Folder parent, IEnumerable<FileSystemMetadata> fileSystemEntries, bool parseName)
         {
             var files = new List<FileSystemMetadata>();
             var items = new List<BaseItem>();
@@ -176,15 +168,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
                 {
                     leftOver.Add(child);
                 }
-                else if (!IsIgnored(child.Name))
+                else
                 {
                     files.Add(child);
                 }
             }
 
-            var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
-
-            var resolver = new AudioBookListResolver(namingOptions);
+            var resolver = new AudioBookListResolver(_namingOptions);
             var resolverResult = resolver.Resolve(files).ToList();
 
             var result = new MultiItemResolverResult
@@ -210,7 +200,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
                 var firstMedia = resolvedItem.Files[0];
 
-                var libraryItem = new T
+                var libraryItem = new AudioBook
                 {
                     Path = firstMedia.Path,
                     IsInMixedFolder = isInMixedFolder,
@@ -230,12 +220,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             return result;
         }
 
-        private bool ContainsFile(List<AudioBookInfo> result, FileSystemMetadata file)
+        private static bool ContainsFile(IEnumerable<AudioBookInfo> result, FileSystemMetadata file)
         {
             return result.Any(i => ContainsFile(i, file));
         }
 
-        private bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
+        private static bool ContainsFile(AudioBookInfo result, FileSystemMetadata file)
         {
             return result.Files.Any(i => ContainsFile(i, file)) ||
                 result.AlternateVersions.Any(i => ContainsFile(i, file)) ||
@@ -246,10 +236,5 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         {
             return string.Equals(result.Path, file.FullName, StringComparison.OrdinalIgnoreCase);
         }
-
-        private static bool IsIgnored(string filename)
-        {
-            return false;
-        }
     }
 }

+ 12 - 18
Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
 using Emby.Naming.Audio;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Providers;
@@ -22,20 +23,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     public class MusicAlbumResolver : ItemResolver<MusicAlbum>
     {
         private readonly ILogger<MusicAlbumResolver> _logger;
-        private readonly IFileSystem _fileSystem;
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicAlbumResolver"/> class.
         /// </summary>
         /// <param name="logger">The logger.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, IFileSystem fileSystem, ILibraryManager libraryManager)
+        /// <param name="namingOptions">The naming options.</param>
+        public MusicAlbumResolver(ILogger<MusicAlbumResolver> logger, NamingOptions namingOptions)
         {
             _logger = logger;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
         }
 
         /// <summary>
@@ -87,7 +85,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         /// <returns><c>true</c> if the provided path points to a music album, <c>false</c> otherwise.</returns>
         public bool IsMusicAlbum(string path, IDirectoryService directoryService)
         {
-            return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService, _logger, _fileSystem, _libraryManager);
+            return ContainsMusic(directoryService.GetFileSystemEntries(path), true, directoryService);
         }
 
         /// <summary>
@@ -101,7 +99,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
             if (args.IsDirectory)
             {
                 // if (args.Parent is MusicArtist) return true;  // saves us from testing children twice
-                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService, _logger, _fileSystem, _libraryManager))
+                if (ContainsMusic(args.FileSystemChildren, true, args.DirectoryService))
                 {
                     return true;
                 }
@@ -116,13 +114,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
         private bool ContainsMusic(
             IEnumerable<FileSystemMetadata> list,
             bool allowSubfolders,
-            IDirectoryService directoryService,
-            ILogger<MusicAlbumResolver> logger,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager)
+            IDirectoryService directoryService)
         {
             // check for audio files before digging down into directories
-            var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && libraryManager.IsAudioFile(fileSystemInfo.FullName));
+            var foundAudioFile = list.Any(fileSystemInfo => !fileSystemInfo.IsDirectory && AudioFileParser.IsAudioFile(fileSystemInfo.FullName, _namingOptions));
             if (foundAudioFile)
             {
                 // at least one audio file exists
@@ -137,21 +132,20 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var discSubfolderCount = 0;
 
-            var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
-            var parser = new AlbumParser(namingOptions);
+            var parser = new AlbumParser(_namingOptions);
 
             var directories = list.Where(fileSystemInfo => fileSystemInfo.IsDirectory);
 
             var result = Parallel.ForEach(directories, (fileSystemInfo, state) =>
             {
                 var path = fileSystemInfo.FullName;
-                var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService, logger, fileSystem, libraryManager);
+                var hasMusic = ContainsMusic(directoryService.GetFileSystemEntries(path), false, directoryService);
 
                 if (hasMusic)
                 {
                     if (parser.IsMultiPart(path))
                     {
-                        logger.LogDebug("Found multi-disc folder: " + path);
+                        _logger.LogDebug("Found multi-disc folder: {Path}", path);
                         Interlocked.Increment(ref discSubfolderCount);
                     }
                     else

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

@@ -3,6 +3,7 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
@@ -19,27 +20,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
     public class MusicArtistResolver : ItemResolver<MusicArtist>
     {
         private readonly ILogger<MusicAlbumResolver> _logger;
-        private readonly IFileSystem _fileSystem;
-        private readonly ILibraryManager _libraryManager;
-        private readonly IServerConfigurationManager _config;
+        private NamingOptions _namingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MusicArtistResolver"/> class.
         /// </summary>
         /// <param name="logger">The logger for the created <see cref="MusicAlbumResolver"/> instances.</param>
-        /// <param name="fileSystem">The file system.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        /// <param name="config">The configuration manager.</param>
+        /// <param name="namingOptions">The naming options.</param>
         public MusicArtistResolver(
             ILogger<MusicAlbumResolver> logger,
-            IFileSystem fileSystem,
-            ILibraryManager libraryManager,
-            IServerConfigurationManager config)
+            NamingOptions namingOptions)
         {
             _logger = logger;
-            _fileSystem = fileSystem;
-            _libraryManager = libraryManager;
-            _config = config;
+            _namingOptions = namingOptions;
         }
 
         /// <summary>
@@ -89,7 +82,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
 
             var directoryService = args.DirectoryService;
 
-            var albumResolver = new MusicAlbumResolver(_logger, _fileSystem, _libraryManager);
+            var albumResolver = new MusicAlbumResolver(_logger, _namingOptions);
 
             // If we contain an album assume we are an artist folder
             var directories = args.FileSystemChildren.Where(i => i.IsDirectory);

+ 7 - 6
Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs

@@ -6,6 +6,7 @@ using System;
 using System.IO;
 using System.Linq;
 using DiscUtils.Udf;
+using Emby.Naming.Common;
 using Emby.Naming.Video;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -21,12 +22,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
     public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T>
         where T : Video, new()
     {
-        protected BaseVideoResolver(ILibraryManager libraryManager)
+        protected BaseVideoResolver(NamingOptions namingOptions)
         {
-            LibraryManager = libraryManager;
+            NamingOptions = namingOptions;
         }
 
-        protected ILibraryManager LibraryManager { get; }
+        protected NamingOptions NamingOptions { get; }
 
         /// <summary>
         /// Resolves the specified args.
@@ -48,7 +49,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
         protected virtual TVideoType ResolveVideo<TVideoType>(ItemResolveArgs args, bool parseName)
               where TVideoType : Video, new()
         {
-            var namingOptions = LibraryManager.GetNamingOptions();
+            var namingOptions = NamingOptions;
 
             // If the path is a file check for a matching extensions
             if (args.IsDirectory)
@@ -138,7 +139,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
                     return null;
                 }
 
-                if (LibraryManager.IsVideoFile(args.Path) || videoInfo.IsStub)
+                if (VideoResolver.IsVideoFile(args.Path, NamingOptions) || videoInfo.IsStub)
                 {
                     var path = args.Path;
 
@@ -267,7 +268,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
         protected void Set3DFormat(Video video)
         {
-            var result = Format3DParser.Parse(video.Path, LibraryManager.GetNamingOptions());
+            var result = Format3DParser.Parse(video.Path, NamingOptions);
 
             Set3DFormat(video, result.Is3D, result.Format3D);
         }

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

@@ -2,16 +2,16 @@
 
 #pragma warning disable CS1591
 
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
 
 namespace Emby.Server.Implementations.Library.Resolvers
 {
     public class GenericVideoResolver<T> : BaseVideoResolver<T>
         where T : Video, new()
     {
-        public GenericVideoResolver(ILibraryManager libraryManager)
-            : base(libraryManager)
+        public GenericVideoResolver(NamingOptions namingOptions)
+            : base(namingOptions)
         {
         }
     }

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

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
+using Emby.Naming.Common;
 using Emby.Naming.Video;
 using Jellyfin.Extensions;
 using MediaBrowser.Controller.Drawing;
@@ -25,6 +26,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
     public class MovieResolver : BaseVideoResolver<Video>, IMultiItemResolver
     {
         private readonly IImageProcessor _imageProcessor;
+        private readonly StackResolver _stackResolver;
 
         private string[] _validCollectionTypes = new[]
         {
@@ -38,12 +40,13 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
         /// <summary>
         /// Initializes a new instance of the <see cref="MovieResolver"/> class.
         /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
         /// <param name="imageProcessor">The image processor.</param>
-        public MovieResolver(ILibraryManager libraryManager, IImageProcessor imageProcessor)
-            : base(libraryManager)
+        /// <param name="namingOptions">The naming options.</param>
+        public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
+            : base(namingOptions)
         {
             _imageProcessor = imageProcessor;
+            _stackResolver = new StackResolver(NamingOptions);
         }
 
         /// <summary>
@@ -89,9 +92,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                     return null;
                 }
 
-                var files = args.FileSystemChildren
-                    .Where(i => !LibraryManager.IgnoreFile(i, args.Parent))
-                    .ToList();
+                var files = args.GetActualFileSystemChildren().ToList();
 
                 if (string.Equals(collectionType, CollectionType.MusicVideos, StringComparison.OrdinalIgnoreCase))
                 {
@@ -258,9 +259,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 }
             }
 
-            var namingOptions = LibraryManager.GetNamingOptions();
-
-            var resolverResult = VideoListResolver.Resolve(files, namingOptions, suppportMultiEditions).ToList();
+            var resolverResult = VideoListResolver.Resolve(files, NamingOptions, suppportMultiEditions).ToList();
 
             var result = new MultiItemResolverResult
             {
@@ -438,7 +437,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
             if (result.Items.Count == 1)
             {
                 var videoPath = result.Items[0].Path;
-                var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(LibraryManager, videoPath, i.Name));
+                var hasPhotos = photos.Any(i => !PhotoResolver.IsOwnedByResolvedMedia(videoPath, i.Name));
 
                 if (!hasPhotos)
                 {
@@ -511,9 +510,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
                 return null;
             }
 
-            var namingOptions = ((LibraryManager)LibraryManager).GetNamingOptions();
-
-            var result = new StackResolver(namingOptions).ResolveDirectories(folderPaths).ToList();
+            var result = _stackResolver.ResolveDirectories(folderPaths).ToList();
 
             if (result.Count != 1)
             {

+ 6 - 5
Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs

@@ -1,6 +1,7 @@
 #nullable disable
 
 using System;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -15,17 +16,17 @@ namespace Emby.Server.Implementations.Library.Resolvers
     public class PhotoAlbumResolver : GenericFolderResolver<PhotoAlbum>
     {
         private readonly IImageProcessor _imageProcessor;
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PhotoAlbumResolver"/> class.
         /// </summary>
         /// <param name="imageProcessor">The image processor.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public PhotoAlbumResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+        /// <param name="namingOptions">The naming options.</param>
+        public PhotoAlbumResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
         {
             _imageProcessor = imageProcessor;
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
         }
 
         /// <inheritdoc />
@@ -73,7 +74,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
                     foreach (var siblingFile in files)
                     {
-                        if (PhotoResolver.IsOwnedByMedia(_libraryManager, siblingFile.FullName, filename))
+                        if (PhotoResolver.IsOwnedByMedia(_namingOptions, siblingFile.FullName, filename))
                         {
                             ownedByMedia = true;
                             break;

+ 11 - 12
Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs

@@ -6,6 +6,8 @@ using System;
 using System.Collections.Generic;
 using System.IO;
 using System.Linq;
+using Emby.Naming.Common;
+using Emby.Naming.Video;
 using MediaBrowser.Controller.Drawing;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Library;
@@ -16,7 +18,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
     public class PhotoResolver : ItemResolver<Photo>
     {
         private readonly IImageProcessor _imageProcessor;
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
+
         private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
         {
             "folder",
@@ -30,10 +33,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
             "default"
         };
 
-        public PhotoResolver(IImageProcessor imageProcessor, ILibraryManager libraryManager)
+
+        public PhotoResolver(IImageProcessor imageProcessor, NamingOptions namingOptions)
         {
             _imageProcessor = imageProcessor;
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
         }
 
         /// <summary>
@@ -60,7 +64,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
 
                         foreach (var file in files)
                         {
-                            if (IsOwnedByMedia(_libraryManager, file.FullName, filename))
+                            if (IsOwnedByMedia(_namingOptions, file.FullName, filename))
                             {
                                 return null;
                             }
@@ -77,17 +81,12 @@ namespace Emby.Server.Implementations.Library.Resolvers
             return null;
         }
 
-        internal static bool IsOwnedByMedia(ILibraryManager libraryManager, string file, string imageFilename)
+        internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
         {
-            if (libraryManager.IsVideoFile(file))
-            {
-                return IsOwnedByResolvedMedia(libraryManager, file, imageFilename);
-            }
-
-            return false;
+            return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
         }
 
-        internal static bool IsOwnedByResolvedMedia(ILibraryManager libraryManager, string file, string imageFilename)
+        internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
             => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
 
         internal static bool IsImageFile(string path, IImageProcessor imageProcessor)

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

@@ -2,6 +2,7 @@
 
 using System;
 using System.Linq;
+using Emby.Naming.Common;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -17,9 +18,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         /// <summary>
         /// Initializes a new instance of the <see cref="EpisodeResolver"/> class.
         /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
-        public EpisodeResolver(ILibraryManager libraryManager)
-            : base(libraryManager)
+        /// <param name="namingOptions">The naming options.</param>
+        public EpisodeResolver(NamingOptions namingOptions)
+            : base(namingOptions)
         {
         }
 

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

@@ -1,6 +1,7 @@
 #nullable disable
 
 using System.Globalization;
+using Emby.Naming.Common;
 using Emby.Naming.TV;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
@@ -14,22 +15,22 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
     /// </summary>
     public class SeasonResolver : GenericFolderResolver<Season>
     {
-        private readonly ILibraryManager _libraryManager;
         private readonly ILocalizationManager _localization;
         private readonly ILogger<SeasonResolver> _logger;
+        private readonly NamingOptions _namingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SeasonResolver"/> class.
         /// </summary>
-        /// <param name="libraryManager">The library manager.</param>
+        /// <param name="namingOptions">The naming options.</param>
         /// <param name="localization">The localization.</param>
         /// <param name="logger">The logger.</param>
         public SeasonResolver(
-            ILibraryManager libraryManager,
+            NamingOptions namingOptions,
             ILocalizationManager localization,
             ILogger<SeasonResolver> logger)
         {
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
             _localization = localization;
             _logger = logger;
         }
@@ -43,7 +44,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
         {
             if (args.Parent is Series series && args.IsDirectory)
             {
-                var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions();
+                var namingOptions = _namingOptions;
 
                 var path = args.Path;
 
@@ -65,18 +66,15 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
 
                     var episodeInfo = resolver.Resolve(testPath, true);
 
-                    if (episodeInfo != null)
+                    if (episodeInfo?.EpisodeNumber != null && episodeInfo.SeasonNumber.HasValue)
                     {
-                        if (episodeInfo.EpisodeNumber.HasValue && episodeInfo.SeasonNumber.HasValue)
-                        {
-                            _logger.LogDebug(
-                                "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
-                                path,
-                                episodeInfo.SeasonNumber.Value,
-                                episodeInfo.EpisodeNumber.Value);
-
-                            return null;
-                        }
+                        _logger.LogDebug(
+                            "Found folder underneath series with episode number: {0}. Season {1}. Episode {2}",
+                            path,
+                            episodeInfo.SeasonNumber.Value,
+                            episodeInfo.EpisodeNumber.Value);
+
+                        return null;
                     }
                 }
 

+ 22 - 16
Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs

@@ -5,7 +5,12 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
+using System.Linq;
+using Emby.Naming.Common;
 using Emby.Naming.TV;
+using Emby.Naming.Video;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities.TV;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Resolvers;
@@ -21,17 +26,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
     public class SeriesResolver : GenericFolderResolver<Series>
     {
         private readonly ILogger<SeriesResolver> _logger;
-        private readonly ILibraryManager _libraryManager;
+        private readonly NamingOptions _namingOptions;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="SeriesResolver"/> class.
         /// </summary>
         /// <param name="logger">The logger.</param>
-        /// <param name="libraryManager">The library manager.</param>
-        public SeriesResolver(ILogger<SeriesResolver> logger, ILibraryManager libraryManager)
+        /// <param name="namingOptions">The naming options.</param>
+        public SeriesResolver(ILogger<SeriesResolver> logger,  NamingOptions namingOptions)
         {
             _logger = logger;
-            _libraryManager = libraryManager;
+            _namingOptions = namingOptions;
         }
 
         /// <summary>
@@ -54,16 +59,19 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                     return null;
                 }
 
+                var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
+
                 var collectionType = args.GetCollectionType();
                 if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase))
                 {
-                    var configuredContentType = _libraryManager.GetConfiguredContentType(args.Path);
+                    // 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))
                     {
                         return new Series
                         {
                             Path = args.Path,
-                            Name = Path.GetFileName(args.Path)
+                            Name = seriesInfo.Name
                         };
                     }
                 }
@@ -80,7 +88,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                         return new Series
                         {
                             Path = args.Path,
-                            Name = Path.GetFileName(args.Path)
+                            Name = seriesInfo.Name
                         };
                     }
 
@@ -89,12 +97,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                         return null;
                     }
 
-                    if (IsSeriesFolder(args.Path, args.FileSystemChildren, _logger, _libraryManager, false))
+                    if (IsSeriesFolder(args.Path, args.FileSystemChildren, false))
                     {
                         return new Series
                         {
                             Path = args.Path,
-                            Name = Path.GetFileName(args.Path)
+                            Name = seriesInfo.Name
                         };
                     }
                 }
@@ -103,11 +111,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
             return null;
         }
 
-        public static bool IsSeriesFolder(
+        private bool IsSeriesFolder(
             string path,
             IEnumerable<FileSystemMetadata> fileSystemChildren,
-            ILogger<SeriesResolver> logger,
-            ILibraryManager libraryManager,
             bool isTvContentType)
         {
             foreach (var child in fileSystemChildren)
@@ -116,21 +122,21 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 {
                     if (IsSeasonFolder(child.FullName, isTvContentType))
                     {
-                        logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
+                        _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName);
                         return true;
                     }
                 }
                 else
                 {
                     string fullName = child.FullName;
-                    if (libraryManager.IsVideoFile(fullName))
+                    if (VideoResolver.IsVideoFile(path, _namingOptions))
                     {
                         if (isTvContentType)
                         {
                             return true;
                         }
 
-                        var namingOptions = ((LibraryManager)libraryManager).GetNamingOptions();
+                        var namingOptions = _namingOptions;
 
                         var episodeResolver = new Naming.TV.EpisodeResolver(namingOptions);
 
@@ -143,7 +149,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
                 }
             }
 
-            logger.LogDebug("{Path} is not a series folder.", path);
+            _logger.LogDebug("{Path} is not a series folder.", path);
             return false;
         }
 

+ 156 - 0
Emby.Server.Implementations/Library/Validators/CollectionPostScanTask.cs

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

+ 1 - 1
Emby.Server.Implementations/Library/Validators/PeopleValidator.cs

@@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Validators
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error validating IBN entry {person}", person);
+                    _logger.LogError(ex, "Error validating IBN entry {Person}", person);
                 }
 
                 // Update progress

+ 6 - 6
Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs

@@ -957,7 +957,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
         public async Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken)
         {
-            _logger.LogInformation("Streaming Channel " + channelId);
+            _logger.LogInformation("Streaming Channel {Id}", channelId);
 
             var result = string.IsNullOrEmpty(streamId) ?
                 null :
@@ -1027,7 +1027,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
         {
             var stream = new MediaSourceInfo
             {
-                EncoderPath = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
+                EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream",
                 EncoderProtocol = MediaProtocol.Http,
                 Path = info.Path,
                 Protocol = MediaProtocol.File,
@@ -1308,16 +1308,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false);
 
                 recordingStatus = RecordingStatus.Completed;
-                _logger.LogInformation("Recording completed: {recordPath}", recordPath);
+                _logger.LogInformation("Recording completed: {RecordPath}", recordPath);
             }
             catch (OperationCanceledException)
             {
-                _logger.LogInformation("Recording stopped: {recordPath}", recordPath);
+                _logger.LogInformation("Recording stopped: {RecordPath}", recordPath);
                 recordingStatus = RecordingStatus.Completed;
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error recording to {recordPath}", recordPath);
+                _logger.LogError(ex, "Error recording to {RecordPath}", recordPath);
                 recordingStatus = RecordingStatus.Error;
             }
 
@@ -1404,7 +1404,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path);
+                    _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path);
                 }
             }
         }

+ 9 - 10
Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs

@@ -87,8 +87,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 ErrorDialog = false
             };
 
-            var commandLineLogMessage = processStartInfo.FileName + " " + processStartInfo.Arguments;
-            _logger.LogInformation(commandLineLogMessage);
+            _logger.LogInformation("{Filename} {Arguments}", processStartInfo.FileName, processStartInfo.Arguments);
 
             var logFilePath = Path.Combine(_appPaths.LogDirectoryPath, "record-transcode-" + Guid.NewGuid() + ".txt");
             Directory.CreateDirectory(Path.GetDirectoryName(logFilePath));
@@ -97,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             _logFileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
 
             await JsonSerializer.SerializeAsync(_logFileStream, mediaSource, _jsonOptions, cancellationToken).ConfigureAwait(false);
-            await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
+            await _logFileStream.WriteAsync(Encoding.UTF8.GetBytes(Environment.NewLine + Environment.NewLine + processStartInfo.FileName + " " + processStartInfo.Arguments + Environment.NewLine + Environment.NewLine), cancellationToken).ConfigureAwait(false);
 
             _process = new Process
             {
@@ -188,7 +187,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 CultureInfo.InvariantCulture,
                 "-i \"{0}\" {2} -map_metadata -1 -threads {6} {3}{4}{5} -y \"{1}\"",
                 inputTempFile,
-                targetFile.Replace("\"", "\\\""), // Escape quotes in filename
+                targetFile.Replace("\"", "\\\"", StringComparison.Ordinal), // Escape quotes in filename
                 videoArgs,
                 GetAudioArgs(mediaSource),
                 subtitleArgs,
@@ -225,13 +224,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
             {
                 try
                 {
-                    _logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath);
+                    _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath);
 
                     _process.StandardInput.WriteLine("q");
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath);
+                    _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath);
                 }
 
                 if (_hasExited)
@@ -241,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 try
                 {
-                    _logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath);
+                    _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath);
 
                     if (_process.WaitForExit(10000))
                     {
@@ -250,7 +249,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath);
+                    _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath);
                 }
 
                 if (_hasExited)
@@ -260,13 +259,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
 
                 try
                 {
-                    _logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath);
+                    _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath);
 
                     _process.Kill();
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath);
+                    _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath);
                 }
             }
         }

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

@@ -11,6 +11,7 @@ using System.Net;
 using System.Net.Http;
 using System.Net.Http.Headers;
 using System.Net.Mime;
+using System.Security.Cryptography;
 using System.Text;
 using System.Text.Json;
 using System.Threading;
@@ -20,7 +21,6 @@ using Jellyfin.Extensions;
 using Jellyfin.Extensions.Json;
 using MediaBrowser.Common.Net;
 using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.LiveTv;
@@ -35,7 +35,6 @@ namespace Emby.Server.Implementations.LiveTv.Listings
         private readonly ILogger<SchedulesDirect> _logger;
         private readonly IHttpClientFactory _httpClientFactory;
         private readonly SemaphoreSlim _tokenSemaphore = new SemaphoreSlim(1, 1);
-        private readonly ICryptoProvider _cryptoProvider;
 
         private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>();
         private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
@@ -43,12 +42,10 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
         public SchedulesDirect(
             ILogger<SchedulesDirect> logger,
-            IHttpClientFactory httpClientFactory,
-            ICryptoProvider cryptoProvider)
+            IHttpClientFactory httpClientFactory)
         {
             _logger = logger;
             _httpClientFactory = httpClientFactory;
-            _cryptoProvider = cryptoProvider;
         }
 
         /// <inheritdoc />
@@ -169,12 +166,12 @@ namespace Emby.Server.Implementations.LiveTv.Listings
 
                         const double DesiredAspect = 2.0 / 3;
 
-                        programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, true, DesiredAspect) ??
-                                                    GetProgramImage(ApiUrl, allImages, true, DesiredAspect);
+                        programEntry.PrimaryImage = GetProgramImage(ApiUrl, imagesWithText, DesiredAspect) ??
+                                                    GetProgramImage(ApiUrl, allImages, DesiredAspect);
 
                         const double WideAspect = 16.0 / 9;
 
-                        programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, true, WideAspect);
+                        programEntry.ThumbImage = GetProgramImage(ApiUrl, imagesWithText, WideAspect);
 
                         // Don't supply the same image twice
                         if (string.Equals(programEntry.PrimaryImage, programEntry.ThumbImage, StringComparison.Ordinal))
@@ -182,7 +179,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
                             programEntry.ThumbImage = null;
                         }
 
-                        programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, true, WideAspect);
+                        programEntry.BackdropImage = GetProgramImage(ApiUrl, imagesWithoutText, WideAspect);
 
                         // programEntry.bannerImage = GetProgramImage(ApiUrl, data, "Banner", false) ??
                         //    GetProgramImage(ApiUrl, data, "Banner-L1", false) ??
@@ -403,7 +400,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             return info;
         }
 
-        private string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, bool returnDefaultImage, double desiredAspect)
+        private static string GetProgramImage(string apiUrl, IEnumerable<ImageDataDto> images, double desiredAspect)
         {
             var match = images
                 .OrderBy(i => Math.Abs(desiredAspect - GetAspectRatio(i)))
@@ -648,7 +645,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
             CancellationToken cancellationToken)
         {
             using var options = new HttpRequestMessage(HttpMethod.Post, ApiUrl + "/token");
-            var hashedPasswordBytes = _cryptoProvider.ComputeHash("SHA1", Encoding.ASCII.GetBytes(password), Array.Empty<byte>());
+            var hashedPasswordBytes = SHA1.HashData(Encoding.ASCII.GetBytes(password));
             // TODO: remove ToLower when Convert.ToHexString supports lowercase
             // Schedules Direct requires the hex to be lowercase
             string hashedPassword = Convert.ToHexString(hashedPasswordBytes).ToLowerInvariant();

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

@@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv
             }
             catch (Exception ex)
             {
-                _logger.LogError(ex, "Error getting image info for {name}", info.Name);
+                _logger.LogError(ex, "Error getting image info for {Name}", info.Name);
             }
 
             return null;

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

@@ -1054,7 +1054,7 @@ namespace Emby.Server.Implementations.LiveTv
             {
                 cancellationToken.ThrowIfCancellationRequested();
 
-                _logger.LogDebug("Refreshing guide from {name}", service.Name);
+                _logger.LogDebug("Refreshing guide from {Name}", service.Name);
 
                 try
                 {
@@ -1135,7 +1135,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name);
+                    _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name);
                 }
 
                 numComplete++;
@@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv
                 }
                 catch (Exception ex)
                 {
-                    _logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name);
+                    _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name);
                 }
 
                 numComplete++;

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

@@ -104,7 +104,7 @@ namespace Emby.Server.Implementations.LiveTv
                 // Dummy this up so that direct play checks can still run
                 if (string.IsNullOrEmpty(source.Path) && source.Protocol == MediaProtocol.Http)
                 {
-                    source.Path = _appHost.GetSmartApiUrl(string.Empty);
+                    source.Path = _appHost.GetApiUrlForLocalAccess();
                 }
             }
 

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

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

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

@@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
 
             Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
 
-            Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host);
+            Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host);
 
             var remoteAddress = IPAddress.Parse(uri.Host);
             IPAddress localAddress = null;
@@ -147,7 +147,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
             // OpenedMediaSource.Path = tempFile;
             // OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
             // OpenedMediaSource.SupportsDirectPlay = false;
             // OpenedMediaSource.SupportsDirectStream = true;

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs

@@ -283,7 +283,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             // #EXTINF:0,84.0 - VOX Schweiz
             if (!string.IsNullOrWhiteSpace(nameInExtInf))
             {
-                var numberIndex = nameInExtInf.IndexOf(' ');
+                var numberIndex = nameInExtInf.IndexOf(' ', StringComparison.Ordinal);
                 if (numberIndex > 0)
                 {
                     var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' });

+ 1 - 1
Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs

@@ -83,7 +83,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
             // OpenedMediaSource.Path = tempFile;
             // OpenedMediaSource.ReadAtNativeFramerate = true;
 
-            MediaSource.Path = _appHost.GetLoopbackHttpApiUrl() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
+            MediaSource.Path = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveStreamFiles/" + UniqueId + "/stream.ts";
             MediaSource.Protocol = MediaProtocol.Http;
 
             // OpenedMediaSource.Path = TempFilePath;

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

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

+ 4 - 0
Emby.Server.Implementations/Localization/Core/be.json

@@ -0,0 +1,4 @@
+{
+    "Sync": "Сінхранізацыя",
+    "Playlists": "Плэйліст"
+}

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

@@ -25,7 +25,7 @@
     "HeaderLiveTV": "TV en Directe",
     "HeaderNextUp": "A continuació",
     "HeaderRecordingGroups": "Grups d'Enregistrament",
-    "HomeVideos": "Vídeos domèstics",
+    "HomeVideos": "Vídeos Domèstics",
     "Inherit": "Hereta",
     "ItemAddedWithName": "{0} ha estat afegit a la biblioteca",
     "ItemRemovedWithName": "{0} ha estat eliminat de la biblioteca",
@@ -39,7 +39,7 @@
     "MixedContent": "Contingut barrejat",
     "Movies": "Pel·lícules",
     "Music": "Música",
-    "MusicVideos": "Vídeos musicals",
+    "MusicVideos": "Vídeos Musicals",
     "NameInstallFailed": "Instalació de {0} fallida",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonUnknown": "Temporada Desconeguda",

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

@@ -15,7 +15,7 @@
     "Favorites": "Oblíbené",
     "Folders": "Složky",
     "Genres": "Žánry",
-    "HeaderAlbumArtists": "Album umělce",
+    "HeaderAlbumArtists": "Umělci alba",
     "HeaderContinueWatching": "Pokračovat ve sledování",
     "HeaderFavoriteAlbums": "Oblíbená alba",
     "HeaderFavoriteArtists": "Oblíbení interpreti",

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

@@ -15,7 +15,7 @@
     "Favorites": "Favourites",
     "Folders": "Folders",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artist's Album",
+    "HeaderAlbumArtists": "Album artists",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favourite Albums",
     "HeaderFavoriteArtists": "Favourite Artists",

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

@@ -12,12 +12,12 @@
     "Default": "Default",
     "DeviceOfflineWithName": "{0} has disconnected",
     "DeviceOnlineWithName": "{0} is connected",
-    "FailedLoginAttemptWithUserName": "Failed login attempt from {0}",
+    "FailedLoginAttemptWithUserName": "Failed login try from {0}",
     "Favorites": "Favorites",
     "Folders": "Folders",
     "Forced": "Forced",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artist's Album",
+    "HeaderAlbumArtists": "Album artists",
     "HeaderContinueWatching": "Continue Watching",
     "HeaderFavoriteAlbums": "Favorite Albums",
     "HeaderFavoriteArtists": "Favorite Artists",

+ 9 - 9
Emby.Server.Implementations/Localization/Core/eo.json

@@ -1,5 +1,5 @@
 {
-    "NotificationOptionInstallationFailed": "Instalada fiasko",
+    "NotificationOptionInstallationFailed": "Instalada malsukceso",
     "NotificationOptionAudioPlaybackStopped": "Ludado de sono haltis",
     "NotificationOptionAudioPlayback": "Ludado de sono lanĉis",
     "NameSeasonUnknown": "Sezono Nekonata",
@@ -11,7 +11,7 @@
     "ItemAddedWithName": "{0} aldonis al la plurmediteko",
     "HeaderLiveTV": "TV-etero",
     "HeaderContinueWatching": "Daŭrigi Spektadon",
-    "HeaderAlbumArtists": "Albumo de artisto",
+    "HeaderAlbumArtists": "Artistoj de albumo",
     "Folders": "Dosierujoj",
     "DeviceOnlineWithName": "{0} estas konektita",
     "Default": "Defaŭlte",
@@ -48,17 +48,17 @@
     "Shows": "Serioj",
     "HeaderFavoriteShows": "Favorataj Serioj",
     "TvShows": "TV-serioj",
-    "Favorites": "Favoratoj",
+    "Favorites": "Favorataj",
     "TaskCleanLogs": "Purigi Ĵurnalan Katalogon",
-    "TaskRefreshLibrary": "Skanu Plurmeditekon",
+    "TaskRefreshLibrary": "Skani Plurmeditekon",
     "ValueSpecialEpisodeName": "Speciala - {0}",
-    "TaskOptimizeDatabase": "Optimigi datumbazon",
+    "TaskOptimizeDatabase": "Optimumigi datenbazon",
     "TaskRefreshChannels": "Refreŝigi Kanalojn",
     "TaskUpdatePlugins": "Ĝisdatigi Kromprogramojn",
     "TaskRefreshPeople": "Refreŝigi Homojn",
     "TasksChannelsCategory": "Interretaj Kanaloj",
     "ProviderValue": "Provizanto: {0}",
-    "NotificationOptionPluginError": "Kromprograma malsukceso",
+    "NotificationOptionPluginError": "Kromprogramo malsukcesis",
     "MixedContent": "Miksita enhavo",
     "TasksApplicationCategory": "Aplikaĵo",
     "TasksMaintenanceCategory": "Prizorgado",
@@ -75,7 +75,7 @@
     "ServerNameNeedsToBeRestarted": "{0} devas esti relanĉita",
     "NotificationOptionVideoPlayback": "La videoludado lanĉis",
     "NotificationOptionServerRestartRequired": "Servila relanĉigo bezonata",
-    "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la teka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
+    "TaskOptimizeDatabaseDescription": "Kompaktigas datenbazon kaj trunkas liberan lokon. Lanĉi ĉi tiun taskon post la plurmediteka skanado aŭ fari aliajn ŝanĝojn, kiuj implicas datenbazajn modifojn, povus plibonigi rendimenton.",
     "TaskUpdatePluginsDescription": "Elŝutas kaj instalas ĝisdatigojn por kromprogramojn, kiuj estas agorditaj por ĝisdatigi aŭtomate.",
     "TaskDownloadMissingSubtitlesDescription": "Serĉas en interreto mankantajn subtekstojn surbaze de metadatena agordaro.",
     "TaskRefreshPeopleDescription": "Ĝisdatigas metadatenojn por aktoroj kaj reĵisoroj en via plurmediteko.",
@@ -102,9 +102,9 @@
     "MessageApplicationUpdatedTo": "Jellyfin Server estis ĝisdatigita al {0}",
     "MessageApplicationUpdated": "Jellyfin Server estis ĝisdatigita",
     "TaskRefreshChannelsDescription": "Refreŝigas informon pri interretaj kanaloj.",
-    "TaskDownloadMissingSubtitles": "Elŝutu mankantajn subtekstojn",
+    "TaskDownloadMissingSubtitles": "Elŝuti mankantajn subtekstojn",
     "TaskCleanTranscode": "Malplenigi Transkodadan Katalogon",
-    "TaskRefreshChapterImages": "Eltiru Ĉapitro-Bildojn",
+    "TaskRefreshChapterImages": "Eltiri Ĉapitraj Bildojn",
     "TaskCleanCache": "Malplenigi Staplan Katalogon",
     "TaskCleanActivityLog": "Malplenigi Aktivecan Ĵurnalon",
     "PluginUpdatedWithName": "{0} estis ĝisdatigita",

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

@@ -15,7 +15,7 @@
     "HeaderFavoriteEpisodes": "Episodios favoritos",
     "HeaderFavoriteShows": "Programas favoritos",
     "HeaderContinueWatching": "Continuar viendo",
-    "HeaderAlbumArtists": "Artistas del álbum",
+    "HeaderAlbumArtists": "Artistas de álbum",
     "Genres": "Géneros",
     "Folders": "Carpetas",
     "Favorites": "Favoritos",
@@ -29,7 +29,7 @@
     "TaskRefreshChannelsDescription": "Actualiza la información de canales de Internet.",
     "TaskRefreshChannels": "Actualizar canales",
     "TaskCleanTranscodeDescription": "Elimina archivos transcodificados que tengan más de un día.",
-    "TaskCleanTranscode": "Limpiar directorio de transcodificado",
+    "TaskCleanTranscode": "Limpiar el directorio de transcodificaciones",
     "TaskUpdatePluginsDescription": "Descarga e instala actualizaciones para complementos que están configurados para actualizarse automáticamente.",
     "TaskUpdatePlugins": "Actualizar complementos",
     "TaskRefreshPeopleDescription": "Actualiza metadatos de actores y directores en tu biblioteca de medios.",
@@ -105,7 +105,7 @@
     "Inherit": "Heredar",
     "HomeVideos": "Videos caseros",
     "HeaderRecordingGroups": "Grupos de grabación",
-    "FailedLoginAttemptWithUserName": "Intento fallido de inicio de sesión desde {0}",
+    "FailedLoginAttemptWithUserName": "Intento de inicio de sesión fallido desde {0}",
     "DeviceOnlineWithName": "{0} está conectado",
     "DeviceOfflineWithName": "{0} se ha desconectado",
     "ChapterNameValue": "Capítulo {0}",
@@ -114,10 +114,10 @@
     "Application": "Aplicación",
     "AppDeviceValues": "App: {0}, Dispositivo: {1}",
     "TaskCleanActivityLogDescription": "Elimina las entradas del registro de actividad anteriores al periodo configurado.",
-    "TaskCleanActivityLog": "Limpiar Registro de Actividades",
+    "TaskCleanActivityLog": "Limpiar registro de actividades",
     "Undefined": "Sin definir",
     "Forced": "Forzado",
-    "Default": "Por Defecto",
-    "TaskOptimizeDatabaseDescription": "Compacta la base de datos y restaura el espacio libre. Ejecutar esta tarea después de actualizar las librerías o realizar otros cambios que impliquen modificar las bases de datos puede mejorar la performance.",
-    "TaskOptimizeDatabase": "Optimización de base de datos"
+    "Default": "Por defecto",
+    "TaskOptimizeDatabaseDescription": "Compacta la base de datos y libera espacio. Ejecutar esta tarea después de escanear la biblioteca o hacer otros cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.",
+    "TaskOptimizeDatabase": "Optimizar base de datos"
 }

+ 15 - 10
Emby.Server.Implementations/Localization/Core/et.json

@@ -17,7 +17,7 @@
     "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.",
     "TaskCleanLogs": "Puhasta logikataloog",
     "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.",
-    "Collections": "Kollektsioonid",
+    "Collections": "Kogumikud",
     "TaskRefreshLibrary": "Skaneeri meediakogu",
     "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.",
     "TaskRefreshChapterImages": "Eralda peatükipildid",
@@ -32,9 +32,9 @@
     "ValueSpecialEpisodeName": "Eriepisood - {0}",
     "ValueHasBeenAddedToLibrary": "{0} lisati meediakogusse",
     "UserStartedPlayingItemWithValues": "{0} taasesitab {1} serveris {2}",
-    "UserPasswordChangedWithName": "Kasutaja {0} parooli on muudetud",
-    "UserLockedOutWithName": "Kasutaja {0} on lukustatud",
-    "UserDeletedWithName": "Kasutaja {0} on kustutatud",
+    "UserPasswordChangedWithName": "Kasutaja {0} parool muudeti",
+    "UserLockedOutWithName": "Kasutaja {0} lukustati",
+    "UserDeletedWithName": "Kasutaja {0} kustutati",
     "UserCreatedWithName": "Kasutaja {0} on loodud",
     "ScheduledTaskStartedWithName": "{0} käivitati",
     "ProviderValue": "Allikas: {0}",
@@ -54,9 +54,9 @@
     "Plugin": "Plugin",
     "Playlists": "Pleilistid",
     "Photos": "Fotod",
-    "NotificationOptionVideoPlaybackStopped": "Video taasesitus on peatatud",
+    "NotificationOptionVideoPlaybackStopped": "Video taasesitus lõppes",
     "NotificationOptionVideoPlayback": "Video taasesitus algas",
-    "NotificationOptionUserLockedOut": "Kasutaja on lukustatud",
+    "NotificationOptionUserLockedOut": "Kasutaja lukustati",
     "NotificationOptionTaskFailed": "Ajastatud ülesanne nurjus",
     "NotificationOptionServerRestartRequired": "Vajalik on serveri taaskäivitamine",
     "NotificationOptionPluginUpdateInstalled": "Paigaldati plugina uuendus",
@@ -66,7 +66,7 @@
     "NotificationOptionNewLibraryContent": "Lisati uut sisu",
     "NotificationOptionInstallationFailed": "Paigaldamine nurjus",
     "NotificationOptionCameraImageUploaded": "Kaamera pilt on üles laaditud",
-    "NotificationOptionAudioPlaybackStopped": "Heli taasesitus peatati",
+    "NotificationOptionAudioPlaybackStopped": "Heli taasesitus lõppes",
     "NotificationOptionAudioPlayback": "Heli taasesitus algas",
     "NotificationOptionApplicationUpdateInstalled": "Rakenduse uuendus paigaldati",
     "NotificationOptionApplicationUpdateAvailable": "Rakenduse uuendus on saadaval",
@@ -97,12 +97,12 @@
     "HeaderFavoriteArtists": "Lemmikesitajad",
     "HeaderFavoriteAlbums": "Lemmikalbumid",
     "HeaderContinueWatching": "Jätka vaatamist",
-    "HeaderAlbumArtists": "Albumi esitaja",
+    "HeaderAlbumArtists": "Albumi esitajad",
     "Genres": "Žanrid",
     "Forced": "Sunnitud",
     "Folders": "Kaustad",
     "Favorites": "Lemmikud",
-    "FailedLoginAttemptWithUserName": "Ebaõnnestunud sisselogimiskatse kasutajalt {0}",
+    "FailedLoginAttemptWithUserName": "{0} - sisselogimine nurjus",
     "DeviceOnlineWithName": "{0} on ühendatud",
     "DeviceOfflineWithName": "{0} katkestas ühenduse",
     "Default": "Vaikimisi",
@@ -114,5 +114,10 @@
     "Artists": "Esitajad",
     "Application": "Rakendus",
     "AppDeviceValues": "Rakendus: {0}, seade: {1}",
-    "Albums": "Albumid"
+    "Albums": "Albumid",
+    "UserOfflineFromDevice": "{0} katkestas ühenduse seadmega {1}",
+    "SubtitleDownloadFailureFromForItem": "Subtiitrite allalaadimine {0} > {1} nurjus",
+    "UserPolicyUpdatedWithName": "Kasutaja {0} õigusi värskendati",
+    "UserStoppedPlayingItemWithValues": "{0} lõpetas {1} taasesituse seadmes {2}",
+    "UserOnlineFromDevice": "{0} on ühendatud seadmest {1}"
 }

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

@@ -6,7 +6,7 @@
     "AuthenticationSucceededWithUserName": "{0} با موفقیت تایید اعتبار شد",
     "Books": "کتاب‌ها",
     "CameraImageUploadedFrom": "یک عکس جدید از دوربین ارسال شده است {0}",
-    "Channels": "کانالها",
+    "Channels": "کانالها",
     "ChapterNameValue": "قسمت {0}",
     "Collections": "مجموعه‌ها",
     "DeviceOfflineWithName": "ارتباط {0} قطع شد",
@@ -37,7 +37,7 @@
     "MessageNamedServerConfigurationUpdatedWithValue": "پکربندی بخش {0} سرور بروزرسانی شد",
     "MessageServerConfigurationUpdated": "پیکربندی سرور بروزرسانی شد",
     "MixedContent": "محتوای مخلوط",
-    "Movies": "فیلمها",
+    "Movies": "فیلم ها",
     "Music": "موسیقی",
     "MusicVideos": "موزیک ویدیوها",
     "NameInstallFailed": "{0} نصب با مشکل مواجه شد",

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

@@ -15,7 +15,7 @@
     "Favorites": "Favoris",
     "Folders": "Dossiers",
     "Genres": "Genres",
-    "HeaderAlbumArtists": "Artistes de l'album",
+    "HeaderAlbumArtists": "Artistes d'album",
     "HeaderContinueWatching": "Continuer à regarder",
     "HeaderFavoriteAlbums": "Albums favoris",
     "HeaderFavoriteArtists": "Artistes préférés",

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

@@ -11,11 +11,11 @@
     "Collections": "Gyűjtemények",
     "DeviceOfflineWithName": "{0} kijelentkezett",
     "DeviceOnlineWithName": "{0} belépett",
-    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet tőle: {0}",
+    "FailedLoginAttemptWithUserName": "Sikertelen bejelentkezési kísérlet innen: {0}",
     "Favorites": "Kedvencek",
     "Folders": "Könyvtárak",
     "Genres": "Műfajok",
-    "HeaderAlbumArtists": "Előadó albumai",
+    "HeaderAlbumArtists": "Album előadó(k)",
     "HeaderContinueWatching": "Megtekintés folytatása",
     "HeaderFavoriteAlbums": "Kedvenc albumok",
     "HeaderFavoriteArtists": "Kedvenc előadók",

+ 7 - 5
Emby.Server.Implementations/Localization/Core/id.json

@@ -7,10 +7,10 @@
     "MessageApplicationUpdated": "Jellyfin Server sudah diperbarui",
     "Latest": "Terbaru",
     "LabelIpAddressValue": "Alamat IP: {0}",
-    "ItemRemovedWithName": "{0} sudah dikeluarkan dari pustaka",
+    "ItemRemovedWithName": "{0} sudah dihapus dari pustaka",
     "ItemAddedWithName": "{0} telah dimasukkan ke dalam pustaka",
-    "Inherit": "Warisan",
-    "HomeVideos": "Video Rumah",
+    "Inherit": "Warisi",
+    "HomeVideos": "Video Rumahan",
     "HeaderRecordingGroups": "Grup Rekaman",
     "HeaderNextUp": "Selanjutnya",
     "HeaderLiveTV": "TV Live",
@@ -73,7 +73,7 @@
     "NotificationOptionCameraImageUploaded": "Gambar kamera terunggah",
     "NotificationOptionApplicationUpdateInstalled": "Pembaruan aplikasi terpasang",
     "NotificationOptionApplicationUpdateAvailable": "Pembaruan aplikasi tersedia",
-    "NewVersionIsAvailable": "Versi baru dari Jellyfin Server tersedia untuk diunduh.",
+    "NewVersionIsAvailable": "Versi baru dari Jellyfin Server sudah tersedia untuk diunduh.",
     "NameSeasonUnknown": "Musim tak diketahui",
     "NameSeasonNumber": "Musim {0}",
     "NameInstallFailed": "{0} penginstalan gagal",
@@ -117,5 +117,7 @@
     "TaskCleanActivityLog": "Bersihkan Log Aktivitas",
     "Undefined": "Tidak terdefinisi",
     "Forced": "Dipaksa",
-    "Default": "Bawaan"
+    "Default": "Bawaan",
+    "TaskOptimizeDatabaseDescription": "Rapihkan basis data dan membersihkan ruang kosong. Menjalankan tugas ini setelah memindai pustaka atau melakukan perubahan lain yang menyiratkan modifikasi basis data dapat meningkatkan kinerja.",
+    "TaskOptimizeDatabase": "Optimalkan basis data"
 }

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

@@ -15,7 +15,7 @@
     "Favorites": "Preferiti",
     "Folders": "Cartelle",
     "Genres": "Generi",
-    "HeaderAlbumArtists": "Artisti dell'Album",
+    "HeaderAlbumArtists": "Artisti dell'album",
     "HeaderContinueWatching": "Continua a guardare",
     "HeaderFavoriteAlbums": "Album Preferiti",
     "HeaderFavoriteArtists": "Artisti Preferiti",

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

@@ -15,7 +15,7 @@
     "Favorites": "Tañdaulylar",
     "Folders": "Qaltalar",
     "Genres": "Janrlar",
-    "HeaderAlbumArtists": "Oryndauşynyñ älbomy",
+    "HeaderAlbumArtists": "Älbom oryndauşylary",
     "HeaderContinueWatching": "Qaraudy jalğastyru",
     "HeaderFavoriteAlbums": "Tañdauly älbomdar",
     "HeaderFavoriteArtists": "Tañdauly oryndauşylar",

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

@@ -50,7 +50,7 @@
     "HeaderFavoriteEpisodes": "Омилени Епизоди",
     "HeaderFavoriteArtists": "Омилени Изведувачи",
     "HeaderFavoriteAlbums": "Омилени Албуми",
-    "HeaderContinueWatching": "Продолжи со гледање",
+    "HeaderContinueWatching": "Продолжи со Гледање",
     "HeaderAlbumArtists": "Изведувачи од Албуми",
     "Genres": "Жанрови",
     "Folders": "Папки",

+ 14 - 0
Emby.Server.Implementations/Localization/Core/mn.json

@@ -0,0 +1,14 @@
+{
+    "Books": "Номууд",
+    "HeaderNextUp": "Дараах",
+    "HeaderContinueWatching": "Үргэлжлүүлэн үзэх",
+    "Songs": "Дуунууд",
+    "Playlists": "Тоглуулах жагсаалт",
+    "Movies": "Кино",
+    "Latest": "Сүүлийн үеийн",
+    "Genres": "Төрөл зүйл",
+    "Favorites": "Дуртай",
+    "Collections": "Багц",
+    "Artists": "Зураачуд",
+    "Albums": "Цомгууд"
+}

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

@@ -39,7 +39,7 @@
     "MixedContent": "Kandungan campuran",
     "Movies": "Filem",
     "Music": "Muzik",
-    "MusicVideos": "Muzik video",
+    "MusicVideos": "",
     "NameInstallFailed": "{0} pemasangan gagal",
     "NameSeasonNumber": "Musim {0}",
     "NameSeasonUnknown": "Musim Tidak Diketahui",

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

@@ -15,7 +15,7 @@
     "Favorites": "Ulubione",
     "Folders": "Foldery",
     "Genres": "Gatunki",
-    "HeaderAlbumArtists": "Album artysty",
+    "HeaderAlbumArtists": "Wykonawcy albumów",
     "HeaderContinueWatching": "Kontynuuj odtwarzanie",
     "HeaderFavoriteAlbums": "Ulubione albumy",
     "HeaderFavoriteArtists": "Ulubieni wykonawcy",
@@ -47,7 +47,7 @@
     "NotificationOptionApplicationUpdateAvailable": "Dostępna aktualizacja aplikacji",
     "NotificationOptionApplicationUpdateInstalled": "Zaktualizowano aplikację",
     "NotificationOptionAudioPlayback": "Rozpoczęto odtwarzanie muzyki",
-    "NotificationOptionAudioPlaybackStopped": "Odtwarzane dźwięku zatrzymane",
+    "NotificationOptionAudioPlaybackStopped": "Odtwarzanie dźwięku zatrzymane",
     "NotificationOptionCameraImageUploaded": "Przekazano obraz z urządzenia przenośnego",
     "NotificationOptionInstallationFailed": "Nieudana instalacja",
     "NotificationOptionNewLibraryContent": "Dodano nową zawartość",
@@ -98,7 +98,7 @@
     "TaskRefreshChannels": "Odśwież kanały",
     "TaskCleanTranscodeDescription": "Usuwa transkodowane pliki starsze niż 1 dzień.",
     "TaskCleanTranscode": "Wyczyść folder transkodowania",
-    "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów które są skonfigurowane do automatycznej aktualizacji.",
+    "TaskUpdatePluginsDescription": "Pobiera i instaluje aktualizacje dla pluginów, które są skonfigurowane do automatycznej aktualizacji.",
     "TaskUpdatePlugins": "Aktualizuj pluginy",
     "TaskRefreshPeopleDescription": "Odświeża metadane o aktorów i reżyserów w Twojej bibliotece mediów.",
     "TaskRefreshPeople": "Odśwież obsadę",

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

@@ -1,5 +1,7 @@
 {
     "Books": "Libros",
     "AuthenticationSucceededWithUserName": "{0} autentificado correctamente",
-    "Artists": "Artistas"
+    "Artists": "Artistas",
+    "Songs": "Shantees",
+    "Albums": "Ships"
 }

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

@@ -15,7 +15,7 @@
     "Favorites": "Favoritos",
     "Folders": "Pastas",
     "Genres": "Géneros",
-    "HeaderAlbumArtists": "Artistas do Álbum",
+    "HeaderAlbumArtists": "Álbum do Artista",
     "HeaderContinueWatching": "Continuar a Ver",
     "HeaderFavoriteAlbums": "Álbuns Favoritos",
     "HeaderFavoriteArtists": "Artistas Favoritos",
@@ -39,7 +39,7 @@
     "MixedContent": "Conteúdo Misto",
     "Movies": "Filmes",
     "Music": "Música",
-    "MusicVideos": "Videoclips",
+    "MusicVideos": "Videoclipes",
     "NameInstallFailed": "{0} falha na instalação",
     "NameSeasonNumber": "Temporada {0}",
     "NameSeasonUnknown": "Temporada Desconhecida",
@@ -118,5 +118,7 @@
     "TaskCleanActivityLog": "Limpar registo de atividade",
     "Undefined": "Indefinido",
     "Forced": "Forçado",
-    "Default": "Padrão"
+    "Default": "Padrão",
+    "TaskOptimizeDatabaseDescription": "Base de dados compacta e corta espaço livre. A execução desta tarefa depois de digitalizar a biblioteca ou de fazer outras alterações que impliquem modificações na base de dados pode melhorar o desempenho.",
+    "TaskOptimizeDatabase": "Otimizar base de dados"
 }

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

@@ -31,7 +31,7 @@
     "ItemRemovedWithName": "{0} - изъято из медиатеки",
     "LabelIpAddressValue": "IP-адрес: {0}",
     "LabelRunningTimeValue": "Длительность: {0}",
-    "Latest": "Последнее",
+    "Latest": "Крайнее",
     "MessageApplicationUpdated": "Jellyfin Server был обновлён",
     "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
     "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
@@ -119,6 +119,6 @@
     "Undefined": "Не определено",
     "Forced": "Форсир-ые",
     "Default": "По умолчанию",
-    "TaskOptimizeDatabaseDescription": "Сжимает базу данных и обрезает свободное место. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
+    "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.",
     "TaskOptimizeDatabase": "Оптимизировать базу данных"
 }

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

@@ -39,7 +39,7 @@
     "MixedContent": "Zmiešaný obsah",
     "Movies": "Filmy",
     "Music": "Hudba",
-    "MusicVideos": "Hudobné videá",
+    "MusicVideos": "Hudobné videoklipy",
     "NameInstallFailed": "Inštalácia {0} zlyhala",
     "NameSeasonNumber": "Séria {0}",
     "NameSeasonUnknown": "Neznáma séria",

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

@@ -74,7 +74,7 @@
     "NameSeasonUnknown": "Sezon i panjohur",
     "NameSeasonNumber": "Sezoni {0}",
     "NameInstallFailed": "Instalimi i {0} dështoi",
-    "MusicVideos": "Video muzikore",
+    "MusicVideos": "Video Muzikore",
     "Music": "Muzikë",
     "Movies": "Filmat",
     "MixedContent": "Përmbajtje e përzier",
@@ -96,7 +96,7 @@
     "HeaderFavoriteArtists": "Artistët e preferuar",
     "HeaderFavoriteAlbums": "Albumet e preferuar",
     "HeaderContinueWatching": "Vazhdo të shikosh",
-    "HeaderAlbumArtists": "Artistët e Albumeve",
+    "HeaderAlbumArtists": "Artistët e albumeve",
     "Genres": "Zhanret",
     "Folders": "Skedarët",
     "Favorites": "Të preferuarat",

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

@@ -50,7 +50,7 @@
     "NameSeasonUnknown": "Непозната сезона",
     "NameSeasonNumber": "Сезона {0}",
     "NameInstallFailed": "Инсталација {0} није успела",
-    "MusicVideos": "Музички спотови",
+    "MusicVideos": "Музички видео",
     "Music": "Музика",
     "Movies": "Филмови",
     "MixedContent": "Мешовит садржај",

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

@@ -15,7 +15,7 @@
     "Favorites": "Favoriter",
     "Folders": "Mappar",
     "Genres": "Genrer",
-    "HeaderAlbumArtists": "Artistens album",
+    "HeaderAlbumArtists": "Albumsartister",
     "HeaderContinueWatching": "Fortsätt kolla",
     "HeaderFavoriteAlbums": "Favoritalbum",
     "HeaderFavoriteArtists": "Favoritartister",

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

@@ -85,7 +85,7 @@
     "HeaderFavoriteArtists": "பிடித்த கலைஞர்கள்",
     "HeaderFavoriteAlbums": "பிடித்த ஆல்பங்கள்",
     "HeaderContinueWatching": "தொடர்ந்து பார்",
-    "HeaderAlbumArtists": "இசைக் கலைஞர்கள்",
+    "HeaderAlbumArtists": "கலைஞரின் ஆல்பம்",
     "Genres": "வகைகள்",
     "Favorites": "பிடித்தவை",
     "ChapterNameValue": "அத்தியாயம் {0}",

+ 23 - 0
Emby.Server.Implementations/Localization/Core/te.json

@@ -0,0 +1,23 @@
+{
+    "ValueSpecialEpisodeName": "ప్రత్యేక - {0}",
+    "Sync": "సమకాలీకరించు",
+    "Songs": "పాటలు",
+    "Shows": "ప్రదర్శనలు",
+    "Playlists": "ప్లేజాబితాలు",
+    "Photos": "ఫోటోలు",
+    "MusicVideos": "మ్యూజిక్ వీడియోలు",
+    "Music": "సంగీతం",
+    "Movies": "సినిమాలు",
+    "HeaderContinueWatching": "చూడటం కొనసాగించండి",
+    "HeaderAlbumArtists": "ఆల్బమ్ కళాకారులు",
+    "Genres": "శైలులు",
+    "Forced": "బలవంతంగా",
+    "Folders": "ఫోల్డర్లు",
+    "Favorites": "ఇష్టమైనవి",
+    "Default": "డిఫాల్ట్",
+    "Collections": "సేకరణలు",
+    "Channels": "ఛానెల్‌లు",
+    "Books": "పుస్తకాలు",
+    "Artists": "కళాకారులు",
+    "Albums": "ఆల్బమ్‌లు"
+}

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

@@ -8,7 +8,7 @@
     "CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
     "Channels": "Kanallar",
     "ChapterNameValue": "Bölüm {0}",
-    "Collections": "Koleksiyon",
+    "Collections": "Koleksiyonlar",
     "DeviceOfflineWithName": "{0} bağlantısı kesildi",
     "DeviceOnlineWithName": "{0} bağlı",
     "FailedLoginAttemptWithUserName": "{0} adresinden giriş başarısız oldu",

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

@@ -16,7 +16,7 @@
     "HeaderFavoriteArtists": "Улюблені виконавці",
     "HeaderFavoriteAlbums": "Улюблені альбоми",
     "HeaderContinueWatching": "Продовжити перегляд",
-    "HeaderAlbumArtists": "Виконавці альбомів",
+    "HeaderAlbumArtists": "Виконавці альбому",
     "Genres": "Жанри",
     "Folders": "Каталоги",
     "Favorites": "Улюблені",

+ 6 - 6
Emby.Server.Implementations/Localization/Core/vi.json

@@ -3,7 +3,7 @@
     "Favorites": "Yêu Thích",
     "Folders": "Thư Mục",
     "Genres": "Thể Loại",
-    "HeaderAlbumArtists": "Album Nghệ sĩ",
+    "HeaderAlbumArtists": "Album nghệ sĩ",
     "HeaderContinueWatching": "Xem Tiếp",
     "HeaderLiveTV": "TV Trực Tiếp",
     "Movies": "Phim",
@@ -62,11 +62,11 @@
     "PluginUninstalledWithName": "{0} đã được gỡ bỏ",
     "PluginInstalledWithName": "{0} đã được cài đặt",
     "Plugin": "Plugin",
-    "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng",
+    "NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video",
     "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video",
     "NotificationOptionUserLockedOut": "Người dùng bị khóa",
     "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch",
-    "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server",
+    "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại máy chủ",
     "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt",
     "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin",
     "NotificationOptionPluginInstalled": "Đã cài đặt Plugin",
@@ -75,7 +75,7 @@
     "NotificationOptionInstallationFailed": "Cài đặt thất bại",
     "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh",
     "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng",
-    "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu",
+    "NotificationOptionAudioPlayback": "Đã bắt đầu phát lại âm thanh",
     "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt",
     "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có",
     "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.",
@@ -95,7 +95,7 @@
     "ItemRemovedWithName": "{0} đã xóa khỏi thư viện",
     "ItemAddedWithName": "{0} được thêm vào thư viện",
     "Inherit": "Thừa hưởng",
-    "HomeVideos": "Video nhà",
+    "HomeVideos": "Video Nhà",
     "HeaderRecordingGroups": "Nhóm Ghi Video",
     "HeaderNextUp": "Tiếp Theo",
     "HeaderFavoriteSongs": "Bài Hát Yêu Thích",
@@ -103,7 +103,7 @@
     "HeaderFavoriteEpisodes": "Tập Phim Yêu Thích",
     "HeaderFavoriteArtists": "Nghệ Sĩ Yêu Thích",
     "HeaderFavoriteAlbums": "Album Ưa Thích",
-    "FailedLoginAttemptWithUserName": "Nỗ lực đăng nhập thất bại từ {0}",
+    "FailedLoginAttemptWithUserName": "Đăng nhập không thành công thử từ {0}",
     "DeviceOnlineWithName": "{0} đã kết nối",
     "DeviceOfflineWithName": "{0} đã ngắt kết nối",
     "ChapterNameValue": "Phân Cảnh {0}",

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

@@ -11,7 +11,7 @@
     "Collections": "合集",
     "DeviceOfflineWithName": "{0} 已断开",
     "DeviceOnlineWithName": "{0} 已连接",
-    "FailedLoginAttemptWithUserName": "来自 {0} 的失败登入",
+    "FailedLoginAttemptWithUserName": "从 {0} 尝试登录失败",
     "Favorites": "我的最爱",
     "Folders": "文件夹",
     "Genres": "风格",

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

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

+ 68 - 35
Emby.Server.Implementations/Localization/LocalizationManager.cs

@@ -372,43 +372,76 @@ namespace Emby.Server.Implementations.Localization
         /// <inheritdoc />
         public IEnumerable<LocalizationOption> GetLocalizationOptions()
         {
-            yield return new LocalizationOption("Arabic", "ar");
-            yield return new LocalizationOption("Bulgarian (Bulgaria)", "bg-BG");
-            yield return new LocalizationOption("Catalan", "ca");
-            yield return new LocalizationOption("Chinese Simplified", "zh-CN");
-            yield return new LocalizationOption("Chinese Traditional", "zh-TW");
-            yield return new LocalizationOption("Croatian", "hr");
-            yield return new LocalizationOption("Czech", "cs");
-            yield return new LocalizationOption("Danish", "da");
-            yield return new LocalizationOption("Dutch", "nl");
+            yield return new LocalizationOption("Afrikaans", "af");
+            yield return new LocalizationOption("العربية", "ar");
+            yield return new LocalizationOption("Беларуская", "be");
+            yield return new LocalizationOption("Български", "bg-BG");
+            yield return new LocalizationOption("বাংলা (বাংলাদেশ)", "bn");
+            yield return new LocalizationOption("Català", "ca");
+            yield return new LocalizationOption("Čeština", "cs");
+            yield return new LocalizationOption("Cymraeg", "cy");
+            yield return new LocalizationOption("Dansk", "da");
+            yield return new LocalizationOption("Deutsch", "de");
             yield return new LocalizationOption("English (United Kingdom)", "en-GB");
-            yield return new LocalizationOption("English (United States)", "en-US");
-            yield return new LocalizationOption("French", "fr");
-            yield return new LocalizationOption("French (Canada)", "fr-CA");
-            yield return new LocalizationOption("German", "de");
-            yield return new LocalizationOption("Greek", "el");
-            yield return new LocalizationOption("Hebrew", "he");
-            yield return new LocalizationOption("Hungarian", "hu");
-            yield return new LocalizationOption("Italian", "it");
-            yield return new LocalizationOption("Kazakh", "kk");
-            yield return new LocalizationOption("Korean", "ko");
-            yield return new LocalizationOption("Lithuanian", "lt-LT");
-            yield return new LocalizationOption("Malay", "ms");
-            yield return new LocalizationOption("Norwegian Bokmål", "nb");
-            yield return new LocalizationOption("Persian", "fa");
-            yield return new LocalizationOption("Polish", "pl");
-            yield return new LocalizationOption("Portuguese (Brazil)", "pt-BR");
-            yield return new LocalizationOption("Portuguese (Portugal)", "pt-PT");
-            yield return new LocalizationOption("Russian", "ru");
-            yield return new LocalizationOption("Slovak", "sk");
-            yield return new LocalizationOption("Slovenian (Slovenia)", "sl-SI");
-            yield return new LocalizationOption("Spanish", "es");
-            yield return new LocalizationOption("Spanish (Argentina)", "es-AR");
-            yield return new LocalizationOption("Spanish (Mexico)", "es-MX");
-            yield return new LocalizationOption("Swedish", "sv");
-            yield return new LocalizationOption("Swiss German", "gsw");
-            yield return new LocalizationOption("Turkish", "tr");
+            yield return new LocalizationOption("English", "en-US");
+            yield return new LocalizationOption("Ελληνικά", "el");
+            yield return new LocalizationOption("Esperanto", "eo");
+            yield return new LocalizationOption("Español", "es");
+            yield return new LocalizationOption("Español americano", "es_419");
+            yield return new LocalizationOption("Español (Argentina)", "es-AR");
+            yield return new LocalizationOption("Español (Dominicana)", "es_DO");
+            yield return new LocalizationOption("Español (México)", "es-MX");
+            yield return new LocalizationOption("Eesti", "et");
+            yield return new LocalizationOption("فارسی", "fa");
+            yield return new LocalizationOption("Suomi", "fi");
+            yield return new LocalizationOption("Filipino", "fil");
+            yield return new LocalizationOption("Français", "fr");
+            yield return new LocalizationOption("Français (Canada)", "fr-CA");
+            yield return new LocalizationOption("Galego", "gl");
+            yield return new LocalizationOption("Schwiizerdütsch", "gsw");
+            yield return new LocalizationOption("עִבְרִית", "he");
+            yield return new LocalizationOption("हिन्दी", "hi");
+            yield return new LocalizationOption("Hrvatski", "hr");
+            yield return new LocalizationOption("Magyar", "hu");
+            yield return new LocalizationOption("Bahasa Indonesia", "id");
+            yield return new LocalizationOption("Íslenska", "is");
+            yield return new LocalizationOption("Italiano", "it");
+            yield return new LocalizationOption("日本語", "ja");
+            yield return new LocalizationOption("Qazaqşa", "kk");
+            yield return new LocalizationOption("한국어", "ko");
+            yield return new LocalizationOption("Lietuvių", "lt");
+            yield return new LocalizationOption("Latviešu", "lv");
+            yield return new LocalizationOption("Македонски", "mk");
+            yield return new LocalizationOption("മലയാളം", "ml");
+            yield return new LocalizationOption("मराठी", "mr");
+            yield return new LocalizationOption("Bahasa Melayu", "ms");
+            yield return new LocalizationOption("Norsk bokmål", "nb");
+            yield return new LocalizationOption("नेपाली", "ne");
+            yield return new LocalizationOption("Nederlands", "nl");
+            yield return new LocalizationOption("Norsk nynorsk", "nn");
+            yield return new LocalizationOption("ਪੰਜਾਬੀ", "pa");
+            yield return new LocalizationOption("Polski", "pl");
+            yield return new LocalizationOption("Pirate", "pr");
+            yield return new LocalizationOption("Português", "pt");
+            yield return new LocalizationOption("Português (Brasil)", "pt-BR");
+            yield return new LocalizationOption("Português (Portugal)", "pt-PT");
+            yield return new LocalizationOption("Românește", "ro");
+            yield return new LocalizationOption("Русский", "ru");
+            yield return new LocalizationOption("Slovenčina", "sk");
+            yield return new LocalizationOption("Slovenščina", "sl-SI");
+            yield return new LocalizationOption("Shqip", "sq");
+            yield return new LocalizationOption("Српски", "sr");
+            yield return new LocalizationOption("Svenska", "sv");
+            yield return new LocalizationOption("தமிழ்", "ta");
+            yield return new LocalizationOption("తెలుగు", "te");
+            yield return new LocalizationOption("ภาษาไทย", "th");
+            yield return new LocalizationOption("Türkçe", "tr");
+            yield return new LocalizationOption("Українська", "uk");
+            yield return new LocalizationOption("اُردُو", "ur_PK");
             yield return new LocalizationOption("Tiếng Việt", "vi");
+            yield return new LocalizationOption("汉语 (简化字)", "zh-CN");
+            yield return new LocalizationOption("漢語 (繁体字)", "zh-TW");
+            yield return new LocalizationOption("廣東話 (香港)", "zh-HK");
         }
     }
 }

+ 1 - 1
Emby.Server.Implementations/Localization/countries.json

@@ -630,7 +630,7 @@
         "TwoLetterISORegionName": "MD"
     },
     {
-        "DisplayName": "Réunion",
+        "DisplayName": "Réunion",
         "Name": "RE",
         "ThreeLetterISORegionName": "REU",
         "TwoLetterISORegionName": "RE"

+ 2 - 1
Emby.Server.Implementations/Localization/iso6392.txt

@@ -349,7 +349,8 @@ pli||pi|Pali|pali
 pol||pl|Polish|polonais
 pon|||Pohnpeian|pohnpei
 por||pt|Portuguese|portugais
-pob||pt-br|Portuguese (Brazil)|portugais
+pop||pt-pt|Portuguese (Portugal)|portugais (pt-pt)
+pob||pt-br|Portuguese (Brazil)|portugais (pt-br)
 pra|||Prakrit languages|prâkrit, langues
 pro|||Provençal, Old (to 1500)|provençal ancien (jusqu'à 1500)
 pus||ps|Pushto; Pashto|pachto

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

@@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.Playlists
             var relativeUri = folderUri.MakeRelativeUri(fileAbsoluteUri);
             string relativePath = Uri.UnescapeDataString(relativeUri.ToString());
 
-            if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase))
+            if (fileAbsoluteUri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase))
             {
                 relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
             }

+ 3 - 2
Emby.Server.Implementations/Plugins/PluginManager.cs

@@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.Plugins
                     {
                         assembly = Assembly.LoadFrom(file);
 
-                        assembly.GetExportedTypes();
+                        // Load all required types to verify that the plugin will load
+                        assembly.GetTypes();
                     }
                     catch (FileLoadException ex)
                     {
@@ -134,7 +135,7 @@ namespace Emby.Server.Implementations.Plugins
                         ChangePluginState(plugin, PluginStatus.Malfunctioned);
                         continue;
                     }
-                    catch (TypeLoadException ex) // Undocumented exception
+                    catch (SystemException ex) when (ex is TypeLoadException or ReflectionTypeLoadException) // Undocumented exception
                     {
                         _logger.LogError(ex, "Failed to load assembly {Path}. This error occurs when a plugin references an incompatible version of one of the shared libraries. Disabling plugin.", file);
                         ChangePluginState(plugin, PluginStatus.NotSupported);

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików